@iaforged/context-code 2.3.3 → 2.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/context-bootstrap.js +7 -5
  2. package/dist/src/commands/login/login.js +1 -1
  3. package/dist/src/components/BaseTextInput.js +1 -1
  4. package/dist/src/components/LogoV2/AnimatedClawd.js +1 -1
  5. package/dist/src/components/LogoV2/Clawd.js +1 -1
  6. package/dist/src/components/LogoV2/LogoV2.js +1 -1
  7. package/dist/src/components/LogoV2/WelcomeV2.js +1 -1
  8. package/dist/src/components/PromptInput/PromptInputFooterLeftSide.js +1 -1
  9. package/dist/src/components/SessionTokenFooter.js +1 -1
  10. package/dist/src/components/Spinner.js +1 -1
  11. package/dist/src/components/Stats.js +1 -1
  12. package/dist/src/components/TeleportProgress.js +1 -1
  13. package/dist/src/components/TextInput.js +1 -1
  14. package/dist/src/components/design-system/ThemeProvider.js +1 -1
  15. package/dist/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.js +1 -1
  16. package/dist/src/main.js +1 -1
  17. package/dist/src/query/stopHooks.js +1 -1
  18. package/dist/src/screens/REPL.js +1 -1
  19. package/dist/src/services/PromptSuggestion/promptSuggestion.js +1 -1
  20. package/dist/src/services/analytics/config.js +1 -1
  21. package/dist/src/services/analytics/datadog.js +1 -1
  22. package/dist/src/services/mcp/config.js +1 -1
  23. package/dist/src/services/tips/tipRegistry.js +1 -1
  24. package/dist/src/services/toolUseSummary/toolUseSummaryGenerator.js +1 -1
  25. package/dist/src/tools/BriefTool/UI.js +1 -1
  26. package/dist/src/utils/computerControlMcp/mcpServer.js +1 -1
  27. package/dist/src/utils/computerControlMcp/server/.gitattributes +18 -0
  28. package/dist/src/utils/computerControlMcp/server/Dockerfile +25 -0
  29. package/dist/src/utils/computerControlMcp/server/LICENSE +21 -0
  30. package/dist/src/utils/computerControlMcp/server/MANIFEST.in +10 -0
  31. package/dist/src/utils/computerControlMcp/server/README.md +193 -0
  32. package/dist/src/utils/computerControlMcp/server/demonstration.gif +0 -0
  33. package/dist/src/utils/computerControlMcp/server/icon.png +0 -0
  34. package/dist/src/utils/computerControlMcp/server/pyproject.toml +52 -0
  35. package/dist/src/utils/computerControlMcp/server/smithery.yaml +13 -0
  36. package/dist/src/utils/computerControlMcp/server/src/README.md +12 -0
  37. package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/FZYTK.TTF +0 -0
  38. package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/__init__.py +11 -0
  39. package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/__main__.py +21 -0
  40. package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/cli.py +128 -0
  41. package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/core.py +1008 -0
  42. package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/gui.py +126 -0
  43. package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/server.py +15 -0
  44. package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/test.py +346 -0
  45. package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/test_image.png +0 -0
  46. package/dist/src/utils/computerControlMcp/server/tests/README.md +22 -0
  47. package/dist/src/utils/computerControlMcp/server/tests/conftest.py +10 -0
  48. package/dist/src/utils/computerControlMcp/server/tests/rapidocr_test.py +21 -0
  49. package/dist/src/utils/computerControlMcp/server/tests/run_cli.py +9 -0
  50. package/dist/src/utils/computerControlMcp/server/tests/run_server.py +15 -0
  51. package/dist/src/utils/computerControlMcp/server/tests/setup.py +16 -0
  52. package/dist/src/utils/computerControlMcp/server/tests/test_computer_control.py +161 -0
  53. package/dist/src/utils/computerControlMcp/server/tests/test_screenshot.py +14 -0
  54. package/dist/src/utils/computerControlMcp/server/tests/test_wgc_env_var.py +42 -0
  55. package/dist/src/utils/computerControlMcp/server/tests/test_wgc_screenshot.py +67 -0
  56. package/dist/src/utils/computerControlMcp/server/uv.lock +4986 -0
  57. package/dist/src/utils/computerControlMcp/setup.js +1 -1
  58. package/dist/src/utils/logoV2Utils.js +1 -1
  59. package/dist/src/utils/model/configs.js +1 -1
  60. package/dist/src/utils/model/model.js +1 -1
  61. package/dist/src/utils/model/modelOptions.js +1 -1
  62. package/dist/src/utils/model/providerModels.js +1 -1
  63. package/dist/src/utils/sembleMcp/setup.js +1 -1
  64. package/dist/src/utils/theme.js +1 -1
  65. package/dist/src/utils/themes/bootstrap.js +1 -1
  66. package/dist/src/utils/themes/opencodeMapper.js +1 -1
  67. package/dist/webapp/chunk-VAB2VXFI.js +1 -1
  68. package/dist/webapp/ngsw.json +1 -1
  69. package/dist/webapp/polyfills-7R4CRVNH.js +1 -1
  70. package/package.json +1 -1
@@ -1 +1 @@
1
- import{feature as e}from"../../recovery/bunBundleShim.js";import{createRequire as r}from"module";const s=r(import.meta.url);import{chmod as o,open as t,rename as n,stat as i,unlink as c}from"fs/promises";import a from"lodash-es/mapValues.js";import p from"lodash-es/memoize.js";import{dirname as l,join as f,parse as u}from"path";import{getPlatform as d}from"../../utils/platform.js";import{getPluginErrorMessage as m}from"../../types/plugin.js";import{isClaudeInChromeMCPServer as g}from"../../utils/claudeInChrome/common.js";import{getCurrentProjectConfig as v,getGlobalConfig as M,saveCurrentProjectConfig as S,saveGlobalConfig as h}from"../../utils/config.js";import{getCwd as w}from"../../utils/cwd.js";import{logForDebugging as C}from"../../utils/debug.js";import{getErrnoCode as y}from"../../utils/errors.js";import{getFsImplementation as j}from"../../utils/fsOperations.js";import{safeParseJSON as P}from"../../utils/json.js";import{logError as b}from"../../utils/log.js";import{getPluginMcpServers as x}from"../../utils/plugins/mcpPluginIntegration.js";import{loadAllPluginsCacheOnly as $}from"../../utils/plugins/pluginLoader.js";import{isSettingSourceEnabled as E}from"../../utils/settings/constants.js";import{getManagedFilePath as O}from"../../utils/settings/managedPath.js";import{isRestrictedToPluginOnly as F}from"../../utils/settings/pluginOnlyPolicy.js";import{getInitialSettings as A,getSettingsForSource as B}from"../../utils/settings/settings.js";import{isMcpServerCommandEntry as N,isMcpServerNameEntry as D,isMcpServerUrlEntry as k}from"../../utils/settings/types.js";import{jsonStringify as V}from"../../utils/slowOperations.js";import{logEvent as U}from"../analytics/index.js";import{fetchClaudeAIMcpConfigsIfEligible as _}from"./claudeai.js";import{expandEnvVarsInString as T}from"./envExpansion.js";import{McpJsonConfigSchema as W,McpServerConfigSchema as I}from"./types.js";import{getProjectMcpServerStatus as R}from"./utils.js";export function getEnterpriseMcpFilePath(){return f(O(),"managed-mcp.json")}function addScopeToServers(e,r){if(!e)return{};const s={};for(const[o,t]of Object.entries(e))s[o]={...t,scope:r};return s}async function writeMcpjsonFile(e){const r=f(w(),".mcp.json");let s;try{s=(await i(r)).mode}catch(e){if("ENOENT"!==y(e))throw e}const a=`${r}.tmp.${process.pid}.${Date.now()}`,p=await t(a,"w",s??420);try{await p.writeFile(V(e,null,2),{encoding:"utf8"}),await p.datasync()}finally{await p.close()}try{void 0!==s&&await o(a,s),await n(a,r)}catch(e){try{await c(a)}catch{}throw e}}function getServerCommandArray(e){if(void 0!==e.type&&"stdio"!==e.type)return null;const r=e;return[r.command,...r.args??[]]}function commandArraysMatch(e,r){return e.length===r.length&&e.every((e,s)=>e===r[s])}function getServerUrl(e){return"url"in e?e.url:null}const J=["/v2/session_ingress/shttp/mcp/","/v2/ccr-sessions/"];export function unwrapCcrProxyUrl(e){if(!J.some(r=>e.includes(r)))return e;try{const r=new URL(e);return r.searchParams.get("mcp_url")||e}catch{return e}}export function getMcpServerSignature(e){const r=getServerCommandArray(e);if(r)return`stdio:${V(r)}`;const s=getServerUrl(e);return s?`url:${unwrapCcrProxyUrl(s)}`:null}export function dedupPluginMcpServers(e,r){const s=new Map;for(const[e,o]of Object.entries(r)){const r=getMcpServerSignature(o);r&&!s.has(r)&&s.set(r,e)}const o={},t=[],n=new Map;for(const[r,i]of Object.entries(e)){const e=getMcpServerSignature(i);if(null===e){o[r]=i;continue}const c=s.get(e);if(void 0!==c){C(`Suppressing plugin MCP server "${r}": duplicates manually-configured "${c}"`),t.push({name:r,duplicateOf:c});continue}const a=n.get(e);void 0===a?(n.set(e,r),o[r]=i):(C(`Suppressing plugin MCP server "${r}": duplicates earlier plugin server "${a}"`),t.push({name:r,duplicateOf:a}))}return{servers:o,suppressed:t}}export function dedupClaudeAiMcpServers(e,r){const s=new Map;for(const[e,o]of Object.entries(r)){if(isMcpServerDisabled(e))continue;const r=getMcpServerSignature(o);r&&!s.has(r)&&s.set(r,e)}const o={},t=[];for(const[r,n]of Object.entries(e)){const e=getMcpServerSignature(n),i=null!==e?s.get(e):void 0;void 0===i?o[r]=n:(C(`Suppressing claude.ai connector "${r}": duplicates manually-configured "${i}"`),t.push({name:r,duplicateOf:i}))}return{servers:o,suppressed:t}}function urlMatchesPattern(e,r){const s=function(e){const r=e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${r}$`)}(r);return s.test(e)}function isMcpServerDenied(e,r){const s=A();if(!s.deniedMcpServers)return!1;for(const r of s.deniedMcpServers)if(D(r)&&r.serverName===e)return!0;if(r){const e=getServerCommandArray(r);if(e)for(const r of s.deniedMcpServers)if(N(r)&&commandArraysMatch(r.serverCommand,e))return!0;const o=getServerUrl(r);if(o)for(const e of s.deniedMcpServers)if(k(e)&&urlMatchesPattern(o,e.serverUrl))return!0}return!1}function isMcpServerAllowedByPolicy(e,r){if(isMcpServerDenied(e,r))return!1;const s=shouldAllowManagedMcpServersOnly()?B("policySettings")??{}:A();if(!s.allowedMcpServers)return!0;if(0===s.allowedMcpServers.length)return!1;const o=s.allowedMcpServers.some(N),t=s.allowedMcpServers.some(k);if(r){const n=getServerCommandArray(r),i=getServerUrl(r);if(n){if(o){for(const e of s.allowedMcpServers)if(N(e)&&commandArraysMatch(e.serverCommand,n))return!0;return!1}for(const r of s.allowedMcpServers)if(D(r)&&r.serverName===e)return!0;return!1}if(i){if(t){for(const e of s.allowedMcpServers)if(k(e)&&urlMatchesPattern(i,e.serverUrl))return!0;return!1}for(const r of s.allowedMcpServers)if(D(r)&&r.serverName===e)return!0;return!1}for(const r of s.allowedMcpServers)if(D(r)&&r.serverName===e)return!0;return!1}for(const r of s.allowedMcpServers)if(D(r)&&r.serverName===e)return!0;return!1}export function filterMcpServersByPolicy(e){const r={},s=[];for(const[o,t]of Object.entries(e)){const e=t;"sdk"===e.type||isMcpServerAllowedByPolicy(o,e)?r[o]=t:s.push(o)}return{allowed:r,blocked:s}}function expandEnvVars(e){const r=[];function expandString(e){const{expanded:s,missingVars:o}=T(e);return r.push(...o),s}let s;switch(e.type){case void 0:case"stdio":{const r=e;s={...r,command:expandString(r.command),args:r.args.map(expandString),env:r.env?a(r.env,expandString):void 0};break}case"sse":case"http":case"ws":{const r=e;s={...r,url:expandString(r.url),headers:r.headers?a(r.headers,expandString):void 0};break}case"sse-ide":case"ws-ide":case"sdk":case"claudeai-proxy":s=e}return{expanded:s,missingVars:[...new Set(r)]}}export async function addMcpConfig(r,s,o){if(r.match(/[^a-zA-Z0-9_-]/))throw new Error(`Invalid name ${r}. Names can only contain letters, numbers, hyphens, and underscores.`);if(g(r))throw new Error(`Cannot add MCP server "${r}": this name is reserved.`);if(e("CHICAGO_MCP")){const{isComputerUseMCPServer:e}=await import("../../utils/computerUse/common.js");if(e(r))throw new Error(`Cannot add MCP server "${r}": this name is reserved.`)}const{isDatabaseMCPServer:t}=await import("../../utils/databaseMcp/common.js");if(t(r))throw new Error(`Cannot add MCP server "${r}": this name is reserved.`);if(doesEnterpriseMcpConfigExist())throw new Error("Cannot add MCP server: enterprise MCP configuration is active and has exclusive control over MCP servers");const n=I().safeParse(s);if(!n.success){const e=n.error.issues.map(e=>`${e.path.join(".")}: ${e.message}`).join(", ");throw new Error(`Invalid configuration: ${e}`)}const i=n.data;if(isMcpServerDenied(r,i))throw new Error(`Cannot add MCP server "${r}": server is explicitly blocked by enterprise policy`);if(!isMcpServerAllowedByPolicy(r,i))throw new Error(`Cannot add MCP server "${r}": not allowed by enterprise policy`);switch(o){case"project":{const{servers:e}=getProjectMcpConfigsFromCwd();if(e[r])throw new Error(`MCP server ${r} already exists in .mcp.json`);break}case"user":{const e=M();if(e.mcpServers?.[r])throw new Error(`MCP server ${r} already exists in user config`);break}case"local":{const e=v();if(e.mcpServers?.[r])throw new Error(`MCP server ${r} already exists in local config`);break}case"dynamic":throw new Error("Cannot add MCP server to scope: dynamic");case"enterprise":throw new Error("Cannot add MCP server to scope: enterprise");case"claudeai":throw new Error("Cannot add MCP server to scope: claudeai")}switch(o){case"project":{const{servers:e}=getProjectMcpConfigsFromCwd(),s={};for(const[r,o]of Object.entries(e)){const{scope:e,...t}=o;s[r]=t}s[r]=i;const o={mcpServers:s};try{await writeMcpjsonFile(o)}catch(e){throw new Error(`Failed to write to .mcp.json: ${e}`)}break}case"user":h(e=>({...e,mcpServers:{...e.mcpServers,[r]:i}}));break;case"local":S(e=>({...e,mcpServers:{...e.mcpServers,[r]:i}}));break;default:throw new Error(`Cannot add MCP server to scope: ${o}`)}}export async function removeMcpConfig(e,r){switch(r){case"project":{const{servers:r}=getProjectMcpConfigsFromCwd();if(!r[e])throw new Error(`No MCP server found with name: ${e} in .mcp.json`);const s={};for(const[o,t]of Object.entries(r))if(o!==e){const{scope:e,...r}=t;s[o]=r}const o={mcpServers:s};try{await writeMcpjsonFile(o)}catch(e){throw new Error(`Failed to remove from .mcp.json: ${e}`)}break}case"user":{const r=M();if(!r.mcpServers?.[e])throw new Error(`No user-scoped MCP server found with name: ${e}`);h(r=>{const{[e]:s,...o}=r.mcpServers??{};return{...r,mcpServers:o}});break}case"local":{const r=v();if(!r.mcpServers?.[e])throw new Error(`No project-local MCP server found with name: ${e}`);S(r=>{const{[e]:s,...o}=r.mcpServers??{};return{...r,mcpServers:o}});break}default:throw new Error(`Cannot remove MCP server from scope: ${r}`)}}export function getProjectMcpConfigsFromCwd(){if(!E("projectSettings"))return{servers:{},errors:[]};const e=f(w(),".mcp.json"),{config:r,errors:s}=parseMcpConfigFromFilePath({filePath:e,expandVars:!0,scope:"project"});if(!r){const r=s.filter(e=>!e.message.startsWith("MCP config file not found"));return r.length>0?(C(`MCP config errors for ${e}: ${V(r.map(e=>e.message))}`,{level:"error"}),{servers:{},errors:r}):{servers:{},errors:[]}}return{servers:r.mcpServers?addScopeToServers(r.mcpServers,"project"):{},errors:s||[]}}export function getMcpConfigsByScope(e){const r={project:"projectSettings",user:"userSettings",local:"localSettings"};if(e in r&&!E(r[e]))return{servers:{},errors:[]};switch(e){case"project":{const r={},s=[],o=[];let t=w();for(;t!==u(t).root;)o.push(t),t=l(t);for(const t of o.reverse()){const o=f(t,".mcp.json"),{config:n,errors:i}=parseMcpConfigFromFilePath({filePath:o,expandVars:!0,scope:"project"});if(!n){const e=i.filter(e=>!e.message.startsWith("MCP config file not found"));e.length>0&&(C(`MCP config errors for ${o}: ${V(e.map(e=>e.message))}`,{level:"error"}),s.push(...e));continue}n.mcpServers&&Object.assign(r,addScopeToServers(n.mcpServers,e)),i.length>0&&s.push(...i)}return{servers:r,errors:s}}case"user":{const r=M().mcpServers;if(!r)return{servers:{},errors:[]};const{config:s,errors:o}=parseMcpConfig({configObject:{mcpServers:r},expandVars:!0,scope:"user"});return{servers:addScopeToServers(s?.mcpServers,e),errors:o}}case"local":{const r=v().mcpServers;if(!r)return{servers:{},errors:[]};const{config:s,errors:o}=parseMcpConfig({configObject:{mcpServers:r},expandVars:!0,scope:"local"});return{servers:addScopeToServers(s?.mcpServers,e),errors:o}}case"enterprise":{const r=getEnterpriseMcpFilePath(),{config:s,errors:o}=parseMcpConfigFromFilePath({filePath:r,expandVars:!0,scope:"enterprise"});if(!s){const e=o.filter(e=>!e.message.startsWith("MCP config file not found"));return e.length>0?(C(`Enterprise MCP config errors for ${r}: ${V(e.map(e=>e.message))}`,{level:"error"}),{servers:{},errors:e}):{servers:{},errors:[]}}return{servers:addScopeToServers(s.mcpServers,e),errors:o}}}}export function getMcpConfigByName(e){const{servers:r}=getMcpConfigsByScope("enterprise");if(F("mcp"))return r[e]??null;const{servers:s}=getMcpConfigsByScope("user"),{servers:o}=getMcpConfigsByScope("project"),{servers:t}=getMcpConfigsByScope("local");return r[e]?r[e]:t[e]?t[e]:o[e]?o[e]:s[e]?s[e]:null}export async function getClaudeCodeMcpConfigs(e={},r=Promise.resolve({})){const{servers:s}=getMcpConfigsByScope("enterprise");if(doesEnterpriseMcpConfigExist()){const e={};for(const[r,o]of Object.entries(s))isMcpServerAllowedByPolicy(r,o)&&(e[r]=o);return{servers:e,errors:[]}}const o=F("mcp"),t={servers:{}},{servers:n}=o?t:getMcpConfigsByScope("user"),{servers:i}=o?t:getMcpConfigsByScope("project"),{servers:c}=o?t:getMcpConfigsByScope("local"),a={},p=await $(),l=[];if(p.errors.length>0)for(const e of p.errors)if("mcp-config-invalid"===e.type||"mcpb-download-failed"===e.type||"mcpb-extract-failed"===e.type||"mcpb-invalid-manifest"===e.type){const r=`Plugin MCP loading error - ${e.type}: ${m(e)}`;b(new Error(r))}else{const r=e.type;C(`Plugin not available for MCP: ${e.source} - error type: ${r}`)}const f=await Promise.all(p.enabled.map(e=>x(e,l)));for(const e of f)e&&Object.assign(a,e);if(l.length>0)for(const e of l){const r=`Plugin MCP server error - ${e.type}: ${m(e)}`;b(new Error(r))}const u={};for(const[e,r]of Object.entries(i))"approved"===R(e)&&(u[e]=r);const d=await r,g={};for(const[r,s]of Object.entries({...n,...u,...c,...e,...d}))!isMcpServerDisabled(r)&&isMcpServerAllowedByPolicy(r,s)&&(g[r]=s);const v={},M={};for(const[e,r]of Object.entries(a))isMcpServerDisabled(e)||!isMcpServerAllowedByPolicy(e,r)?M[e]=r:v[e]=r;const{servers:S,suppressed:h}=dedupPluginMcpServers(v,g);Object.assign(S,M);for(const{name:e,duplicateOf:r}of h){const s=e.split(":");"plugin"!==s[0]||s.length<3||l.push({type:"mcp-server-suppressed-duplicate",source:e,plugin:s[1],serverName:s.slice(2).join(":"),duplicateOf:r})}const w=Object.assign({},S,n,u,c),y={};for(const[e,r]of Object.entries(w))isMcpServerAllowedByPolicy(e,r)&&(y[e]=r);return{servers:y,errors:l}}export async function getAllMcpConfigs(){if(doesEnterpriseMcpConfigExist())return getClaudeCodeMcpConfigs();const e=_(),{servers:r,errors:s}=await getClaudeCodeMcpConfigs({},e),{allowed:o}=filterMcpServersByPolicy(await e),{servers:t}=dedupClaudeAiMcpServers(o,r);return{servers:Object.assign({},t,r),errors:s}}export function parseMcpConfig(e){const{configObject:r,expandVars:s,scope:o,filePath:t}=e,n=W().safeParse(r);if(!n.success)return{config:null,errors:n.error.issues.map(e=>({...t&&{file:t},path:e.path.join("."),message:"Does not adhere to MCP server configuration schema",mcpErrorMetadata:{scope:o,severity:"fatal"}}))};const i=[],c={};for(const[e,r]of Object.entries(n.data.mcpServers)){let n=r;if(s){const{expanded:s,missingVars:c}=expandEnvVars(r);c.length>0&&i.push({...t&&{file:t},path:`mcpServers.${e}`,message:`Missing environment variables: ${c.join(", ")}`,suggestion:`Set the following environment variables: ${c.join(", ")}`,mcpErrorMetadata:{scope:o,serverName:e,severity:"warning"}}),n=s}"windows"!==d()||n.type&&"stdio"!==n.type||"npx"!==n.command&&!n.command.endsWith("\\npx")&&!n.command.endsWith("/npx")||i.push({...t&&{file:t},path:`mcpServers.${e}`,message:"Windows requires 'cmd /c' wrapper to execute npx",suggestion:'Change command to "cmd" with args ["/c", "npx", ...]',mcpErrorMetadata:{scope:o,serverName:e,severity:"warning"}}),c[e]=n}return{config:{mcpServers:c},errors:i}}export function parseMcpConfigFromFilePath(e){const{filePath:r,expandVars:s,scope:o}=e,t=j();let n;try{n=t.readFileSync(r,{encoding:"utf8"})}catch(e){return"ENOENT"===y(e)?{config:null,errors:[{file:r,path:"",message:`MCP config file not found: ${r}`,suggestion:"Check that the file path is correct",mcpErrorMetadata:{scope:o,severity:"fatal"}}]}:(C(`MCP config read error for ${r} (scope=${o}): ${e}`,{level:"error"}),{config:null,errors:[{file:r,path:"",message:`Failed to read file: ${e}`,suggestion:"Check file permissions and ensure the file exists",mcpErrorMetadata:{scope:o,severity:"fatal"}}]})}const i=P(n);return i?parseMcpConfig({configObject:i,expandVars:s,scope:o,filePath:r}):(C(`MCP config is not valid JSON: ${r} (scope=${o}, length=${n.length}, first100=${V(n.slice(0,100))})`,{level:"error"}),{config:null,errors:[{file:r,path:"",message:"MCP config is not a valid JSON",suggestion:"Fix the JSON syntax errors in the file",mcpErrorMetadata:{scope:o,severity:"fatal"}}]})}export const doesEnterpriseMcpConfigExist=p(()=>{const{config:e}=parseMcpConfigFromFilePath({filePath:getEnterpriseMcpFilePath(),expandVars:!0,scope:"enterprise"});return null!==e});export function shouldAllowManagedMcpServersOnly(){return!0===B("policySettings")?.allowManagedMcpServersOnly}export function areMcpConfigsAllowedWithEnterpriseMcpConfig(e){return Object.values(e).every(e=>"sdk"===e.type&&"claude-vscode"===e.name)}const z=e("CHICAGO_MCP")?s("../../utils/computerUse/common.js").COMPUTER_USE_MCP_SERVER_NAME:null;function isDefaultDisabledBuiltin(e){return null!==z&&e===z}export function isMcpServerDisabled(e){const r=v();if(isDefaultDisabledBuiltin(e)){return!(r.enabledMcpServers||[]).includes(e)}return(r.disabledMcpServers||[]).includes(e)}function toggleMembership(e,r,s){return e.includes(r)===s?e:s?[...e,r]:e.filter(e=>e!==r)}export function setMcpServerEnabled(e,r){const s=isDefaultDisabledBuiltin(e)&&isMcpServerDisabled(e)===r;S(s=>{if(isDefaultDisabledBuiltin(e)){const o=s.enabledMcpServers||[],t=toggleMembership(o,e,r);return t===o?s:{...s,enabledMcpServers:t}}const o=s.disabledMcpServers||[],t=toggleMembership(o,e,!r);return t===o?s:{...s,disabledMcpServers:t}}),s&&U("tengu_builtin_mcp_toggle",{serverName:e,enabled:r})}
1
+ import{feature as e}from"../../recovery/bunBundleShim.js";import{createRequire as r}from"module";const s=r(import.meta.url);import{chmod as o,open as t,rename as n,stat as i,unlink as c}from"fs/promises";import a from"lodash-es/mapValues.js";import p from"lodash-es/memoize.js";import{dirname as l,join as f,parse as u}from"path";import{getPlatform as d}from"../../utils/platform.js";import{getPluginErrorMessage as m}from"../../types/plugin.js";import{isClaudeInChromeMCPServer as g}from"../../utils/claudeInChrome/common.js";import{getCurrentProjectConfig as v,getGlobalConfig as M,saveCurrentProjectConfig as S,saveGlobalConfig as h}from"../../utils/config.js";import{getCwd as w}from"../../utils/cwd.js";import{logForDebugging as C}from"../../utils/debug.js";import{getErrnoCode as y}from"../../utils/errors.js";import{getFsImplementation as j}from"../../utils/fsOperations.js";import{safeParseJSON as P}from"../../utils/json.js";import{logError as b}from"../../utils/log.js";import{getPluginMcpServers as x}from"../../utils/plugins/mcpPluginIntegration.js";import{loadAllPluginsCacheOnly as $}from"../../utils/plugins/pluginLoader.js";import{isSettingSourceEnabled as E}from"../../utils/settings/constants.js";import{getManagedFilePath as O}from"../../utils/settings/managedPath.js";import{isRestrictedToPluginOnly as F}from"../../utils/settings/pluginOnlyPolicy.js";import{getInitialSettings as A,getSettingsForSource as B}from"../../utils/settings/settings.js";import{isMcpServerCommandEntry as N,isMcpServerNameEntry as D,isMcpServerUrlEntry as k}from"../../utils/settings/types.js";import{jsonStringify as V}from"../../utils/slowOperations.js";import{logEvent as U}from"../analytics/index.js";import{fetchClaudeAIMcpConfigsIfEligible as _}from"./claudeai.js";import{expandEnvVarsInString as T}from"./envExpansion.js";import{McpJsonConfigSchema as W,McpServerConfigSchema as I}from"./types.js";import{getProjectMcpServerStatus as R}from"./utils.js";export function getEnterpriseMcpFilePath(){return f(O(),"managed-mcp.json")}function addScopeToServers(e,r){if(!e)return{};const s={};for(const[o,t]of Object.entries(e))s[o]={...t,scope:r};return s}async function writeMcpjsonFile(e){const r=f(w(),".mcp.json");let s;try{s=(await i(r)).mode}catch(e){if("ENOENT"!==y(e))throw e}const a=`${r}.tmp.${process.pid}.${Date.now()}`,p=await t(a,"w",s??420);try{await p.writeFile(V(e,null,2),{encoding:"utf8"}),await p.datasync()}finally{await p.close()}try{void 0!==s&&await o(a,s),await n(a,r)}catch(e){try{await c(a)}catch{}throw e}}function getServerCommandArray(e){if(void 0!==e.type&&"stdio"!==e.type)return null;const r=e;return[r.command,...r.args??[]]}function commandArraysMatch(e,r){return e.length===r.length&&e.every((e,s)=>e===r[s])}function getServerUrl(e){return"url"in e?e.url:null}const J=["/v2/session_ingress/shttp/mcp/","/v2/ccr-sessions/"];export function unwrapCcrProxyUrl(e){if(!J.some(r=>e.includes(r)))return e;try{const r=new URL(e);return r.searchParams.get("mcp_url")||e}catch{return e}}export function getMcpServerSignature(e){const r=getServerCommandArray(e);if(r)return`stdio:${V(r)}`;const s=getServerUrl(e);return s?`url:${unwrapCcrProxyUrl(s)}`:null}export function dedupPluginMcpServers(e,r){const s=new Map;for(const[e,o]of Object.entries(r)){const r=getMcpServerSignature(o);r&&!s.has(r)&&s.set(r,e)}const o={},t=[],n=new Map;for(const[r,i]of Object.entries(e)){const e=getMcpServerSignature(i);if(null===e){o[r]=i;continue}const c=s.get(e);if(void 0!==c){C(`Suppressing plugin MCP server "${r}": duplicates manually-configured "${c}"`),t.push({name:r,duplicateOf:c});continue}const a=n.get(e);void 0===a?(n.set(e,r),o[r]=i):(C(`Suppressing plugin MCP server "${r}": duplicates earlier plugin server "${a}"`),t.push({name:r,duplicateOf:a}))}return{servers:o,suppressed:t}}export function dedupClaudeAiMcpServers(e,r){const s=new Map;for(const[e,o]of Object.entries(r)){if(isMcpServerDisabled(e))continue;const r=getMcpServerSignature(o);r&&!s.has(r)&&s.set(r,e)}const o={},t=[];for(const[r,n]of Object.entries(e)){const e=getMcpServerSignature(n),i=null!==e?s.get(e):void 0;void 0===i?o[r]=n:(C(`Suppressing claude.ai connector "${r}": duplicates manually-configured "${i}"`),t.push({name:r,duplicateOf:i}))}return{servers:o,suppressed:t}}function urlMatchesPattern(e,r){const s=function(e){const r=e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${r}$`)}(r);return s.test(e)}function isMcpServerDenied(e,r){const s=A();if(!s.deniedMcpServers)return!1;for(const r of s.deniedMcpServers)if(D(r)&&r.serverName===e)return!0;if(r){const e=getServerCommandArray(r);if(e)for(const r of s.deniedMcpServers)if(N(r)&&commandArraysMatch(r.serverCommand,e))return!0;const o=getServerUrl(r);if(o)for(const e of s.deniedMcpServers)if(k(e)&&urlMatchesPattern(o,e.serverUrl))return!0}return!1}function isMcpServerAllowedByPolicy(e,r){if(isMcpServerDenied(e,r))return!1;const s=shouldAllowManagedMcpServersOnly()?B("policySettings")??{}:A();if(!s.allowedMcpServers)return!0;if(0===s.allowedMcpServers.length)return!1;const o=s.allowedMcpServers.some(N),t=s.allowedMcpServers.some(k);if(r){const n=getServerCommandArray(r),i=getServerUrl(r);if(n){if(o){for(const e of s.allowedMcpServers)if(N(e)&&commandArraysMatch(e.serverCommand,n))return!0;return!1}for(const r of s.allowedMcpServers)if(D(r)&&r.serverName===e)return!0;return!1}if(i){if(t){for(const e of s.allowedMcpServers)if(k(e)&&urlMatchesPattern(i,e.serverUrl))return!0;return!1}for(const r of s.allowedMcpServers)if(D(r)&&r.serverName===e)return!0;return!1}for(const r of s.allowedMcpServers)if(D(r)&&r.serverName===e)return!0;return!1}for(const r of s.allowedMcpServers)if(D(r)&&r.serverName===e)return!0;return!1}export function filterMcpServersByPolicy(e){const r={},s=[];for(const[o,t]of Object.entries(e)){const e=t;"sdk"===e.type||isMcpServerAllowedByPolicy(o,e)?r[o]=t:s.push(o)}return{allowed:r,blocked:s}}function expandEnvVars(e){const r=[];function expandString(e){const{expanded:s,missingVars:o}=T(e);return r.push(...o),s}let s;switch(e.type){case void 0:case"stdio":{const r=e;s={...r,command:expandString(r.command),args:r.args.map(expandString),env:r.env?a(r.env,expandString):void 0};break}case"sse":case"http":case"ws":{const r=e;s={...r,url:expandString(r.url),headers:r.headers?a(r.headers,expandString):void 0};break}case"sse-ide":case"ws-ide":case"sdk":case"claudeai-proxy":s=e}return{expanded:s,missingVars:[...new Set(r)]}}export async function addMcpConfig(r,s,o){if(r.match(/[^a-zA-Z0-9_-]/))throw new Error(`Invalid name ${r}. Names can only contain letters, numbers, hyphens, and underscores.`);if(g(r))throw new Error(`Cannot add MCP server "${r}": this name is reserved.`);if(e("CHICAGO_MCP")){const{isComputerUseMCPServer:e}=await import("../../utils/computerUse/common.js");if(e(r))throw new Error(`Cannot add MCP server "${r}": this name is reserved.`)}const{isDatabaseMCPServer:t}=await import("../../utils/databaseMcp/common.js");if(t(r))throw new Error(`Cannot add MCP server "${r}": this name is reserved.`);const{isComputerControlMCPServer:n}=await import("../../utils/computerControlMcp/common.js");if(n(r))throw new Error(`Cannot add MCP server "${r}": this name is reserved.`);if(doesEnterpriseMcpConfigExist())throw new Error("Cannot add MCP server: enterprise MCP configuration is active and has exclusive control over MCP servers");const i=I().safeParse(s);if(!i.success){const e=i.error.issues.map(e=>`${e.path.join(".")}: ${e.message}`).join(", ");throw new Error(`Invalid configuration: ${e}`)}const c=i.data;if(isMcpServerDenied(r,c))throw new Error(`Cannot add MCP server "${r}": server is explicitly blocked by enterprise policy`);if(!isMcpServerAllowedByPolicy(r,c))throw new Error(`Cannot add MCP server "${r}": not allowed by enterprise policy`);switch(o){case"project":{const{servers:e}=getProjectMcpConfigsFromCwd();if(e[r])throw new Error(`MCP server ${r} already exists in .mcp.json`);break}case"user":{const e=M();if(e.mcpServers?.[r])throw new Error(`MCP server ${r} already exists in user config`);break}case"local":{const e=v();if(e.mcpServers?.[r])throw new Error(`MCP server ${r} already exists in local config`);break}case"dynamic":throw new Error("Cannot add MCP server to scope: dynamic");case"enterprise":throw new Error("Cannot add MCP server to scope: enterprise");case"claudeai":throw new Error("Cannot add MCP server to scope: claudeai")}switch(o){case"project":{const{servers:e}=getProjectMcpConfigsFromCwd(),s={};for(const[r,o]of Object.entries(e)){const{scope:e,...t}=o;s[r]=t}s[r]=c;const o={mcpServers:s};try{await writeMcpjsonFile(o)}catch(e){throw new Error(`Failed to write to .mcp.json: ${e}`)}break}case"user":h(e=>({...e,mcpServers:{...e.mcpServers,[r]:c}}));break;case"local":S(e=>({...e,mcpServers:{...e.mcpServers,[r]:c}}));break;default:throw new Error(`Cannot add MCP server to scope: ${o}`)}}export async function removeMcpConfig(e,r){switch(r){case"project":{const{servers:r}=getProjectMcpConfigsFromCwd();if(!r[e])throw new Error(`No MCP server found with name: ${e} in .mcp.json`);const s={};for(const[o,t]of Object.entries(r))if(o!==e){const{scope:e,...r}=t;s[o]=r}const o={mcpServers:s};try{await writeMcpjsonFile(o)}catch(e){throw new Error(`Failed to remove from .mcp.json: ${e}`)}break}case"user":{const r=M();if(!r.mcpServers?.[e])throw new Error(`No user-scoped MCP server found with name: ${e}`);h(r=>{const{[e]:s,...o}=r.mcpServers??{};return{...r,mcpServers:o}});break}case"local":{const r=v();if(!r.mcpServers?.[e])throw new Error(`No project-local MCP server found with name: ${e}`);S(r=>{const{[e]:s,...o}=r.mcpServers??{};return{...r,mcpServers:o}});break}default:throw new Error(`Cannot remove MCP server from scope: ${r}`)}}export function getProjectMcpConfigsFromCwd(){if(!E("projectSettings"))return{servers:{},errors:[]};const e=f(w(),".mcp.json"),{config:r,errors:s}=parseMcpConfigFromFilePath({filePath:e,expandVars:!0,scope:"project"});if(!r){const r=s.filter(e=>!e.message.startsWith("MCP config file not found"));return r.length>0?(C(`MCP config errors for ${e}: ${V(r.map(e=>e.message))}`,{level:"error"}),{servers:{},errors:r}):{servers:{},errors:[]}}return{servers:r.mcpServers?addScopeToServers(r.mcpServers,"project"):{},errors:s||[]}}export function getMcpConfigsByScope(e){const r={project:"projectSettings",user:"userSettings",local:"localSettings"};if(e in r&&!E(r[e]))return{servers:{},errors:[]};switch(e){case"project":{const r={},s=[],o=[];let t=w();for(;t!==u(t).root;)o.push(t),t=l(t);for(const t of o.reverse()){const o=f(t,".mcp.json"),{config:n,errors:i}=parseMcpConfigFromFilePath({filePath:o,expandVars:!0,scope:"project"});if(!n){const e=i.filter(e=>!e.message.startsWith("MCP config file not found"));e.length>0&&(C(`MCP config errors for ${o}: ${V(e.map(e=>e.message))}`,{level:"error"}),s.push(...e));continue}n.mcpServers&&Object.assign(r,addScopeToServers(n.mcpServers,e)),i.length>0&&s.push(...i)}return{servers:r,errors:s}}case"user":{const r=M().mcpServers;if(!r)return{servers:{},errors:[]};const{config:s,errors:o}=parseMcpConfig({configObject:{mcpServers:r},expandVars:!0,scope:"user"});return{servers:addScopeToServers(s?.mcpServers,e),errors:o}}case"local":{const r=v().mcpServers;if(!r)return{servers:{},errors:[]};const{config:s,errors:o}=parseMcpConfig({configObject:{mcpServers:r},expandVars:!0,scope:"local"});return{servers:addScopeToServers(s?.mcpServers,e),errors:o}}case"enterprise":{const r=getEnterpriseMcpFilePath(),{config:s,errors:o}=parseMcpConfigFromFilePath({filePath:r,expandVars:!0,scope:"enterprise"});if(!s){const e=o.filter(e=>!e.message.startsWith("MCP config file not found"));return e.length>0?(C(`Enterprise MCP config errors for ${r}: ${V(e.map(e=>e.message))}`,{level:"error"}),{servers:{},errors:e}):{servers:{},errors:[]}}return{servers:addScopeToServers(s.mcpServers,e),errors:o}}}}export function getMcpConfigByName(e){const{servers:r}=getMcpConfigsByScope("enterprise");if(F("mcp"))return r[e]??null;const{servers:s}=getMcpConfigsByScope("user"),{servers:o}=getMcpConfigsByScope("project"),{servers:t}=getMcpConfigsByScope("local");return r[e]?r[e]:t[e]?t[e]:o[e]?o[e]:s[e]?s[e]:null}export async function getClaudeCodeMcpConfigs(e={},r=Promise.resolve({})){const{servers:s}=getMcpConfigsByScope("enterprise");if(doesEnterpriseMcpConfigExist()){const e={};for(const[r,o]of Object.entries(s))isMcpServerAllowedByPolicy(r,o)&&(e[r]=o);return{servers:e,errors:[]}}const o=F("mcp"),t={servers:{}},{servers:n}=o?t:getMcpConfigsByScope("user"),{servers:i}=o?t:getMcpConfigsByScope("project"),{servers:c}=o?t:getMcpConfigsByScope("local"),a={},p=await $(),l=[];if(p.errors.length>0)for(const e of p.errors)if("mcp-config-invalid"===e.type||"mcpb-download-failed"===e.type||"mcpb-extract-failed"===e.type||"mcpb-invalid-manifest"===e.type){const r=`Plugin MCP loading error - ${e.type}: ${m(e)}`;b(new Error(r))}else{const r=e.type;C(`Plugin not available for MCP: ${e.source} - error type: ${r}`)}const f=await Promise.all(p.enabled.map(e=>x(e,l)));for(const e of f)e&&Object.assign(a,e);if(l.length>0)for(const e of l){const r=`Plugin MCP server error - ${e.type}: ${m(e)}`;b(new Error(r))}const u={};for(const[e,r]of Object.entries(i))"approved"===R(e)&&(u[e]=r);const d=await r,g={};for(const[r,s]of Object.entries({...n,...u,...c,...e,...d}))!isMcpServerDisabled(r)&&isMcpServerAllowedByPolicy(r,s)&&(g[r]=s);const v={},M={};for(const[e,r]of Object.entries(a))isMcpServerDisabled(e)||!isMcpServerAllowedByPolicy(e,r)?M[e]=r:v[e]=r;const{servers:S,suppressed:h}=dedupPluginMcpServers(v,g);Object.assign(S,M);for(const{name:e,duplicateOf:r}of h){const s=e.split(":");"plugin"!==s[0]||s.length<3||l.push({type:"mcp-server-suppressed-duplicate",source:e,plugin:s[1],serverName:s.slice(2).join(":"),duplicateOf:r})}const w=Object.assign({},S,n,u,c),y={};for(const[e,r]of Object.entries(w))isMcpServerAllowedByPolicy(e,r)&&(y[e]=r);return{servers:y,errors:l}}export async function getAllMcpConfigs(){if(doesEnterpriseMcpConfigExist())return getClaudeCodeMcpConfigs();const e=_(),{servers:r,errors:s}=await getClaudeCodeMcpConfigs({},e),{allowed:o}=filterMcpServersByPolicy(await e),{servers:t}=dedupClaudeAiMcpServers(o,r);return{servers:Object.assign({},t,r),errors:s}}export function parseMcpConfig(e){const{configObject:r,expandVars:s,scope:o,filePath:t}=e,n=W().safeParse(r);if(!n.success)return{config:null,errors:n.error.issues.map(e=>({...t&&{file:t},path:e.path.join("."),message:"Does not adhere to MCP server configuration schema",mcpErrorMetadata:{scope:o,severity:"fatal"}}))};const i=[],c={};for(const[e,r]of Object.entries(n.data.mcpServers)){let n=r;if(s){const{expanded:s,missingVars:c}=expandEnvVars(r);c.length>0&&i.push({...t&&{file:t},path:`mcpServers.${e}`,message:`Missing environment variables: ${c.join(", ")}`,suggestion:`Set the following environment variables: ${c.join(", ")}`,mcpErrorMetadata:{scope:o,serverName:e,severity:"warning"}}),n=s}"windows"!==d()||n.type&&"stdio"!==n.type||"npx"!==n.command&&!n.command.endsWith("\\npx")&&!n.command.endsWith("/npx")||i.push({...t&&{file:t},path:`mcpServers.${e}`,message:"Windows requires 'cmd /c' wrapper to execute npx",suggestion:'Change command to "cmd" with args ["/c", "npx", ...]',mcpErrorMetadata:{scope:o,serverName:e,severity:"warning"}}),c[e]=n}return{config:{mcpServers:c},errors:i}}export function parseMcpConfigFromFilePath(e){const{filePath:r,expandVars:s,scope:o}=e,t=j();let n;try{n=t.readFileSync(r,{encoding:"utf8"})}catch(e){return"ENOENT"===y(e)?{config:null,errors:[{file:r,path:"",message:`MCP config file not found: ${r}`,suggestion:"Check that the file path is correct",mcpErrorMetadata:{scope:o,severity:"fatal"}}]}:(C(`MCP config read error for ${r} (scope=${o}): ${e}`,{level:"error"}),{config:null,errors:[{file:r,path:"",message:`Failed to read file: ${e}`,suggestion:"Check file permissions and ensure the file exists",mcpErrorMetadata:{scope:o,severity:"fatal"}}]})}const i=P(n);return i?parseMcpConfig({configObject:i,expandVars:s,scope:o,filePath:r}):(C(`MCP config is not valid JSON: ${r} (scope=${o}, length=${n.length}, first100=${V(n.slice(0,100))})`,{level:"error"}),{config:null,errors:[{file:r,path:"",message:"MCP config is not a valid JSON",suggestion:"Fix the JSON syntax errors in the file",mcpErrorMetadata:{scope:o,severity:"fatal"}}]})}export const doesEnterpriseMcpConfigExist=p(()=>{const{config:e}=parseMcpConfigFromFilePath({filePath:getEnterpriseMcpFilePath(),expandVars:!0,scope:"enterprise"});return null!==e});export function shouldAllowManagedMcpServersOnly(){return!0===B("policySettings")?.allowManagedMcpServersOnly}export function areMcpConfigsAllowedWithEnterpriseMcpConfig(e){return Object.values(e).every(e=>"sdk"===e.type&&"claude-vscode"===e.name)}const z=e("CHICAGO_MCP")?s("../../utils/computerUse/common.js").COMPUTER_USE_MCP_SERVER_NAME:null;function isDefaultDisabledBuiltin(e){return null!==z&&e===z}export function isMcpServerDisabled(e){const r=v();if(isDefaultDisabledBuiltin(e)){return!(r.enabledMcpServers||[]).includes(e)}return(r.disabledMcpServers||[]).includes(e)}function toggleMembership(e,r,s){return e.includes(r)===s?e:s?[...e,r]:e.filter(e=>e!==r)}export function setMcpServerEnabled(e,r){const s=isDefaultDisabledBuiltin(e)&&isMcpServerDisabled(e)===r;S(s=>{if(isDefaultDisabledBuiltin(e)){const o=s.enabledMcpServers||[],t=toggleMembership(o,e,r);return t===o?s:{...s,enabledMcpServers:t}}const o=s.disabledMcpServers||[],t=toggleMembership(o,e,!r);return t===o?s:{...s,disabledMcpServers:t}}),s&&U("tengu_builtin_mcp_toggle",{serverName:e,enabled:r})}
@@ -1 +1 @@
1
- import e from"chalk";import{logForDebugging as n}from"../../utils/debug.js";import{fileHistoryEnabled as a}from"../../utils/fileHistory.js";import{getInitialSettings as s,getSettings_DEPRECATED as t,getSettingsForSource as o}from"../../utils/settings/settings.js";import{shouldOfferTerminalSetup as i}from"../../commands/terminalSetup/terminalSetup.js";import{getDesktopUpsellConfig as r}from"../../components/DesktopUpsell/DesktopUpsellStartup.js";import{color as l}from"../../components/design-system/color.js";import{shouldShowOverageCreditUpsell as c}from"../../components/LogoV2/OverageCreditUpsell.js";import{getShortcutDisplay as d}from"../../keybindings/shortcutFormat.js";import{isKairosCronEnabled as u}from"../../tools/ScheduleCronTool/prompt.js";import{is1PApiCustomer as p}from"../../utils/auth.js";import{countConcurrentSessions as m}from"../../utils/concurrentSessions.js";import{getGlobalConfig as f}from"../../utils/config.js";import{getEffortEnvOverride as y,modelSupportsEffort as g}from"../../utils/effort.js";import{env as v}from"../../utils/env.js";import{cacheKeys as S}from"../../utils/fileStateCache.js";import{getWorktreeCount as h}from"../../utils/git.js";import{detectRunningIDEsCached as w,getSortedIdeLockfiles as R,isCursorInstalled as b,isSupportedTerminal as C,isSupportedVSCodeTerminal as j,isVSCodeInstalled as E,isWindsurfInstalled as P}from"../../utils/ide.js";import{getMainLoopModel as _,getUserSpecifiedModelSetting as T}from"../../utils/model/model.js";import{getPlatform as U}from"../../utils/platform.js";import{isPluginInstalled as k}from"../../utils/plugins/installedPluginsManager.js";import{loadKnownMarketplacesConfigSafe as x}from"../../utils/plugins/marketplaceManager.js";import{OFFICIAL_MARKETPLACE_NAME as O}from"../../utils/plugins/officialMarketplace.js";import{getCurrentSessionAgentColor as $,isCustomTitleEnabled as M}from"../../utils/sessionStorage.js";import{getFeatureValue_CACHED_MAY_BE_STALE as L}from"../analytics/growthbook.js";import{formatGrantAmount as A,getCachedOverageCreditGrant as D}from"../api/overageCreditGrant.js";import{checkCachedPassesEligibility as I,formatCreditAmount as q,getCachedReferrerReward as z}from"../api/referral.js";import{getSessionsSinceLastShown as H}from"./tipHistory.js";let N;async function isMarketplacePluginRelevant(e,n,a){if(!await async function(){if(void 0!==N)return N;const e=await x();return N=O in e,N}())return!1;if(k(`${e}@${O}`))return!1;const{bashTools:s}=n??{};if(a.cli&&s?.size&&a.cli.some(e=>s.has(e)))return!0;if(a.filePath&&n?.readFileState){if(S(n.readFileState).some(e=>a.filePath.test(e)))return!0}return!1}const Y=[{id:"new-user-warmup",content:async()=>"Comienza con pequeñas funciones o correcciones, pide a Context que proponga un plan y verifica sus ediciones sugeridas",cooldownSessions:3,isRelevant:async()=>f().numStartups<10},{id:"plan-mode-for-complex-tasks",content:async()=>`Usa el Modo Plan para preparar una solicitud compleja antes de realizar cambios. Presiona ${d("chat:cycleMode","Chat","shift+tab")} dos veces para activarlo.`,cooldownSessions:5,isRelevant:async()=>{if("ant"===process.env.USER_TYPE)return!1;const e=f();return(e.lastPlanModeUse?(Date.now()-e.lastPlanModeUse)/864e5:1/0)>7}},{id:"default-permission-mode-config",content:async()=>"Usa /config para cambiar tu modo de permisos predeterminado (incluyendo el Modo Plan)",cooldownSessions:10,isRelevant:async()=>{try{const e=f(),n=t(),a=Boolean(e.lastPlanModeUse),s=Boolean(n?.permissions?.defaultMode);return a&&!s}catch(e){return n(`Failed to check default-permission-mode-config tip relevance: ${e}`,{level:"warn"}),!1}}},{id:"git-worktrees",content:async()=>"Usa git worktrees para ejecutar múltiples sesiones de Context en paralelo.",cooldownSessions:10,isRelevant:async()=>{try{const e=f();return await h()<=1&&e.numStartups>50}catch(e){return!1}}},{id:"color-when-multi-clauding",content:async()=>"¿Ejecutando múltiples sesiones de Context? Usa /color y /rename para distinguirlas de un vistazo.",cooldownSessions:10,isRelevant:async()=>{if($())return!1;return await m()>=2}},{id:"terminal-setup",content:async()=>"Apple_Terminal"===v.terminal?"Ejecuta /terminal-setup para habilitar la integración del terminal, como Option + Enter para nueva línea y más":"Ejecuta /terminal-setup para habilitar la integración del terminal, como Shift + Enter para nueva línea y más",cooldownSessions:10,async isRelevant(){const e=f();return"Apple_Terminal"===v.terminal?!e.optionAsMetaKeyInstalled:!e.shiftEnterKeyBindingInstalled}},{id:"shift-enter",content:async()=>"Apple_Terminal"===v.terminal?"Presiona Option+Enter para enviar un mensaje multilínea":"Presiona Shift+Enter para enviar un mensaje multilínea",cooldownSessions:10,async isRelevant(){const e=f();return Boolean(("Apple_Terminal"===v.terminal?e.optionAsMetaKeyInstalled:e.shiftEnterKeyBindingInstalled)&&e.numStartups>3)}},{id:"shift-enter-setup",content:async()=>"Apple_Terminal"===v.terminal?"Ejecuta /terminal-setup para habilitar Option+Enter para nuevas líneas":"Ejecuta /terminal-setup para habilitar Shift+Enter para nuevas líneas",cooldownSessions:10,async isRelevant(){if(!i())return!1;const e=f();return!("Apple_Terminal"===v.terminal?e.optionAsMetaKeyInstalled:e.shiftEnterKeyBindingInstalled)}},{id:"memory-command",content:async()=>"Usa /memory para ver y gestionar la memoria de Context",cooldownSessions:15,isRelevant:async()=>f().memoryUsageCount<=0},{id:"theme-command",content:async()=>"Usa /theme para cambiar el tema de color",cooldownSessions:20,isRelevant:async()=>!0},{id:"colorterm-truecolor",content:async()=>"Prueba a configurar la variable de entorno COLORTERM=truecolor para obtener colores más vivos",cooldownSessions:30,isRelevant:async()=>!process.env.COLORTERM&&e.level<3},{id:"powershell-tool-env",content:async()=>"Configura CONTEXT_CODE_USE_POWERSHELL_TOOL=1 para habilitar la herramienta PowerShell (vista previa)",cooldownSessions:10,isRelevant:async()=>"windows"===U()&&void 0===process.env.CONTEXT_CODE_USE_POWERSHELL_TOOL&&void 0===process.env.CLAUDE_CODE_USE_POWERSHELL_TOOL},{id:"status-line",content:async()=>"Usa /statusline para configurar una línea de estado personalizada que se mostrará debajo del cuadro de entrada",cooldownSessions:25,isRelevant:async()=>void 0===t().statusLine},{id:"prompt-queue",content:async()=>"Pulsa Enter para poner en cola mensajes adicionales mientras Context está trabajando.",cooldownSessions:5,isRelevant:async()=>f().promptQueueUseCount<=3},{id:"enter-to-steer-in-relatime",content:async()=>"Envía mensajes a Context mientras trabaja para guiarlo en tiempo real",cooldownSessions:20,isRelevant:async()=>!0},{id:"todo-list",content:async()=>"Pide a Context que cree una lista de tareas cuando trabajes en tareas complejas para seguir el progreso",cooldownSessions:20,isRelevant:async()=>!0},{id:"vscode-command-install",content:async()=>`Abre la Paleta de Comandos (Cmd+Shift+P) y ejecuta "Shell Command: Install '${"vscode"===v.terminal?"code":v.terminal}' command in PATH" para habilitar la integración con el IDE`,cooldownSessions:0,async isRelevant(){if(!j())return!1;if("macos"!==U())return!1;switch(v.terminal){case"vscode":return!await E();case"cursor":return!await b();case"windsurf":return!await P();default:return!1}}},{id:"ide-upsell-external-terminal",content:async()=>"Conecta Context a tu IDE · /ide",cooldownSessions:4,async isRelevant(){if(C())return!1;if(0!==(await R()).length)return!1;return(await w()).length>0}},{id:"install-github-app",content:async()=>"Ejecuta /install-github-app para etiquetar a @claude directamente desde tus issues y PRs de Github",cooldownSessions:10,isRelevant:async()=>!f().githubActionSetupCount},{id:"install-slack-app",content:async()=>"Ejecuta /install-slack-app para usar Context en Slack",cooldownSessions:10,isRelevant:async()=>!f().slackAppInstallCount},{id:"permissions",content:async()=>"Usa /permissions para pre-aprobar y pre-denegar herramientas de bash, edición y MCP",cooldownSessions:10,isRelevant:async()=>f().numStartups>10},{id:"drag-and-drop-images",content:async()=>"¿Sabías que puedes arrastrar y soltar archivos de imagen en tu terminal?",cooldownSessions:10,isRelevant:async()=>!v.isSSH()},{id:"paste-images-mac",content:async()=>"Pega imágenes en Context Code usando control+v (¡no cmd+v!)",cooldownSessions:10,isRelevant:async()=>"macos"===U()},{id:"double-esc",content:async()=>"Pulsa dos veces esc para rebobinar la conversación a un punto anterior en el tiempo",cooldownSessions:10,isRelevant:async()=>!a()},{id:"double-esc-code-restore",content:async()=>"Pulsa dos veces esc para rebobinar el código y/o la conversación a un punto anterior en el tiempo",cooldownSessions:10,isRelevant:async()=>a()},{id:"continue",content:async()=>"Ejecuta context --continue o context --resume para reanudar una conversación",cooldownSessions:10,isRelevant:async()=>!0},{id:"rename-conversation",content:async()=>"Nombra tus conversaciones con /rename para encontrarlas fácilmente en /resume más tarde",cooldownSessions:15,isRelevant:async()=>M()&&f().numStartups>10},{id:"custom-commands",content:async()=>"Crea habilidades añadiendo archivos .md a .context/skills/ en tu proyecto o ~/.context/skills/ para habilidades globales",cooldownSessions:15,isRelevant:async()=>f().numStartups>10},{id:"shift-tab",content:async()=>"ant"===process.env.USER_TYPE?`Pulsa ${d("chat:cycleMode","Chat","shift+tab")} para alternar entre el modo predeterminado y el modo automático`:`Pulsa ${d("chat:cycleMode","Chat","shift+tab")} para alternar entre el modo predeterminado, el modo de edición con auto-aceptación y el modo de planificación`,cooldownSessions:10,isRelevant:async()=>!0},{id:"image-paste",content:async()=>`Usa ${d("chat:imagePaste","Chat","ctrl+v")} para pegar imágenes desde tu portapapeles`,cooldownSessions:20,isRelevant:async()=>!0},{id:"custom-agents",content:async()=>"Usa /agents para optimizar tareas específicas. Ej: Arquitecto de Software, Escritor de Código, Revisor de Código",cooldownSessions:15,isRelevant:async()=>f().numStartups>5},{id:"agent-flag",content:async()=>"Usa --agent <nombre_agente> para iniciar directamente una conversación con un subagente",cooldownSessions:15,isRelevant:async()=>f().numStartups>5},{id:"desktop-app",content:async()=>"Ejecuta Context Code local o remotamente usando la aplicación de escritorio: clau.de/desktop",cooldownSessions:15,isRelevant:async()=>"linux"!==U()},{id:"desktop-shortcut",content:async e=>`Continúa tu sesión en Context Code Desktop con ${l("suggestion",e.theme)("/desktop")}`,cooldownSessions:15,isRelevant:async()=>!!r().enable_shortcut_tip&&("darwin"===process.platform||"win32"===process.platform&&"x64"===process.arch)},{id:"web-app",content:async()=>"Ejecuta tareas en la nube mientras sigues programando localmente · clau.de/web",cooldownSessions:15,isRelevant:async()=>!0},{id:"mobile-app",content:async()=>"/mobile para usar Context Code desde la aplicación de Context en tu teléfono",cooldownSessions:15,isRelevant:async()=>!0},{id:"opusplan-mode-reminder",content:async()=>`Tu modelo predeterminado es Opus Plan Mode. Presiona ${d("chat:cycleMode","Chat","shift+tab")} dos veces para activar el Modo Plan y planificar con Context Opus.`,cooldownSessions:2,async isRelevant(){if("ant"===process.env.USER_TYPE)return!1;const e=f(),n="opusplan"===T(),a=e.lastPlanModeUse?(Date.now()-e.lastPlanModeUse)/864e5:1/0;return n&&a>3}},{id:"frontend-design-plugin",content:async e=>`¿Trabajando con HTML/CSS? Instala el plugin frontend-design:\n${l("suggestion",e.theme)(`/plugin install frontend-design@${O}`)}`,cooldownSessions:3,isRelevant:async e=>isMarketplacePluginRelevant("frontend-design",e,{filePath:/\.(html|css|htm)$/i})},{id:"vercel-plugin",content:async e=>`¿Trabajando con Vercel? Instala el plugin vercel:\n${l("suggestion",e.theme)(`/plugin install vercel@${O}`)}`,cooldownSessions:3,isRelevant:async e=>isMarketplacePluginRelevant("vercel",e,{filePath:/(?:^|[/\\])vercel\.json$/i,cli:["vercel"]})},{id:"effort-high-nudge",content:async e=>{const n=l("suggestion",e.theme)("/effort high");return"copy_b"===L("tengu_tide_elm","off")?`Usa ${n} para obtener mejores respuestas directas. Context lo analiza profundamente primero.`:`¿Trabajando en algo difícil? ${n} ofrece mejores respuestas iniciales`},cooldownSessions:3,isRelevant:async()=>{if(!p())return!1;if(!g(_()))return!1;if(void 0!==o("policySettings")?.effortLevel)return!1;if(void 0!==y())return!1;const e=s().effortLevel;return"high"!==e&&"max"!==e&&"off"!==L("tengu_tide_elm","off")}},{id:"subagent-fanout-nudge",content:async e=>{const n=l("suggestion",e.theme);return"copy_b"===L("tengu_tern_alloy","off")?`Para tareas grandes, pide a Context que ${n("use subagentes")}. Trabajan en paralelo y mantienen limpia la conversación principal.`:`Di ${n('"fan out subagents"')} y Context enviará un equipo. Cada uno profundiza para que no se escape nada.`},cooldownSessions:3,isRelevant:async()=>!!p()&&"off"!==L("tengu_tern_alloy","off")},{id:"loop-command-nudge",content:async e=>{const n=l("suggestion",e.theme);return"copy_b"===L("tengu_timber_lark","off")?`Usa ${n("/loop 5m check the deploy")} para ejecutar cualquier prompt de forma programada. Configúralo y olvídate.`:`${n("/loop")} ejecuta cualquier prompt de forma recurrente. Ideal para monitorear despliegues o estados de PR.`},cooldownSessions:3,isRelevant:async()=>!!p()&&(!!u()&&"off"!==L("tengu_timber_lark","off"))},{id:"guest-passes",content:async e=>{const n=l("claude",e.theme),a=z();return a?`Comparte Context Code y gana ${n(q(a))} de uso extra · ${n("/passes")}`:`Tienes pases de invitado gratuitos para compartir · ${n("/passes")}`},cooldownSessions:3,isRelevant:async()=>{if(f().hasVisitedPasses)return!1;const{eligible:e}=I();return e}},{id:"overage-credit",content:async e=>{const n=l("claude",e.theme),a=D(),s=a?A(a):null;return s?`${n(`${s} en uso extra, por nuestra cuenta`)} · aplicaciones de terceros · ${n("/extra-usage")}`:""},cooldownSessions:3,isRelevant:async()=>c()},{id:"feedback-command",content:async()=>"¡Usa /feedback para ayudarnos a mejorar!",cooldownSessions:15,async isRelevant(){if("ant"===process.env.USER_TYPE)return!1;return f().numStartups>5}}],B="ant"===process.env.USER_TYPE?[{id:"important-claudemd",content:async()=>'[ANT-ONLY] Use "IMPORTANT:" prefix for must-follow CLAUDE.md rules',cooldownSessions:30,isRelevant:async()=>!0},{id:"skillify",content:async()=>"[ANT-ONLY] Use /skillify at the end of a workflow to turn it into a reusable skill",cooldownSessions:15,isRelevant:async()=>!0}]:[];export async function getRelevantTips(e){const n=s().spinnerTipsOverride,a=function(){const e=s().spinnerTipsOverride;return e?.tips?.length?e.tips.map((e,n)=>({id:`custom-tip-${n}`,content:async()=>e,cooldownSessions:0,isRelevant:async()=>!0})):[]}();if(n?.excludeDefault&&a.length>0)return a;const t=[...Y,...B],o=await Promise.all(t.map(n=>n.isRelevant(e)));return[...t.filter((e,n)=>o[n]).filter(e=>H(e.id)>=e.cooldownSessions),...a]}
1
+ import e from"chalk";import{logForDebugging as n}from"../../utils/debug.js";import{fileHistoryEnabled as a}from"../../utils/fileHistory.js";import{getInitialSettings as s,getSettings_DEPRECATED as t,getSettingsForSource as o}from"../../utils/settings/settings.js";import{shouldOfferTerminalSetup as i}from"../../commands/terminalSetup/terminalSetup.js";import{getDesktopUpsellConfig as r}from"../../components/DesktopUpsell/DesktopUpsellStartup.js";import{color as l}from"../../components/design-system/color.js";import{shouldShowOverageCreditUpsell as c}from"../../components/LogoV2/OverageCreditUpsell.js";import{getShortcutDisplay as d}from"../../keybindings/shortcutFormat.js";import{isKairosCronEnabled as u}from"../../tools/ScheduleCronTool/prompt.js";import{is1PApiCustomer as p}from"../../utils/auth.js";import{countConcurrentSessions as m}from"../../utils/concurrentSessions.js";import{getGlobalConfig as f}from"../../utils/config.js";import{getEffortEnvOverride as y,modelSupportsEffort as g}from"../../utils/effort.js";import{env as v}from"../../utils/env.js";import{cacheKeys as S}from"../../utils/fileStateCache.js";import{getWorktreeCount as h}from"../../utils/git.js";import{detectRunningIDEsCached as w,getSortedIdeLockfiles as R,isCursorInstalled as b,isSupportedTerminal as C,isSupportedVSCodeTerminal as j,isVSCodeInstalled as E,isWindsurfInstalled as P}from"../../utils/ide.js";import{getMainLoopModel as _,getUserSpecifiedModelSetting as T}from"../../utils/model/model.js";import{getPlatform as x}from"../../utils/platform.js";import{isPluginInstalled as U}from"../../utils/plugins/installedPluginsManager.js";import{loadKnownMarketplacesConfigSafe as k}from"../../utils/plugins/marketplaceManager.js";import{OFFICIAL_MARKETPLACE_NAME as O}from"../../utils/plugins/officialMarketplace.js";import{getCurrentSessionAgentColor as $,isCustomTitleEnabled as M}from"../../utils/sessionStorage.js";import{getFeatureValue_CACHED_MAY_BE_STALE as L}from"../analytics/growthbook.js";import{formatGrantAmount as A,getCachedOverageCreditGrant as D}from"../api/overageCreditGrant.js";import{checkCachedPassesEligibility as I,formatCreditAmount as q,getCachedReferrerReward as z}from"../api/referral.js";import{getSessionsSinceLastShown as H}from"./tipHistory.js";let N;async function isMarketplacePluginRelevant(e,n,a){if(!await async function(){if(void 0!==N)return N;const e=await k();return N=O in e,N}())return!1;if(U(`${e}@${O}`))return!1;const{bashTools:s}=n??{};if(a.cli&&s?.size&&a.cli.some(e=>s.has(e)))return!0;if(a.filePath&&n?.readFileState){if(S(n.readFileState).some(e=>a.filePath.test(e)))return!0}return!1}const Y=[{id:"new-user-warmup",content:async()=>"Comienza con pequeñas funciones o correcciones, pide a Context que proponga un plan y verifica sus ediciones sugeridas",cooldownSessions:3,isRelevant:async()=>f().numStartups<10},{id:"plan-mode-for-complex-tasks",content:async()=>`Usa el Modo Plan para preparar una solicitud compleja antes de realizar cambios. Presiona ${d("chat:cycleMode","Chat","shift+tab")} dos veces para activarlo.`,cooldownSessions:5,isRelevant:async()=>{if("ant"===process.env.USER_TYPE)return!1;const e=f();return(e.lastPlanModeUse?(Date.now()-e.lastPlanModeUse)/864e5:1/0)>7}},{id:"default-permission-mode-config",content:async()=>"Usa /config para cambiar tu modo de permisos predeterminado (incluyendo el Modo Plan)",cooldownSessions:10,isRelevant:async()=>{try{const e=f(),n=t(),a=Boolean(e.lastPlanModeUse),s=Boolean(n?.permissions?.defaultMode);return a&&!s}catch(e){return n(`Failed to check default-permission-mode-config tip relevance: ${e}`,{level:"warn"}),!1}}},{id:"git-worktrees",content:async()=>"Usa git worktrees para ejecutar múltiples sesiones de Context en paralelo.",cooldownSessions:10,isRelevant:async()=>{try{const e=f();return await h()<=1&&e.numStartups>50}catch(e){return!1}}},{id:"color-when-multi-clauding",content:async()=>"¿Ejecutando múltiples sesiones de Context? Usa /color y /rename para distinguirlas de un vistazo.",cooldownSessions:10,isRelevant:async()=>{if($())return!1;return await m()>=2}},{id:"terminal-setup",content:async()=>"Apple_Terminal"===v.terminal?"Ejecuta /terminal-setup para habilitar la integración del terminal, como Option + Enter para nueva línea y más":"Ejecuta /terminal-setup para habilitar la integración del terminal, como Shift + Enter para nueva línea y más",cooldownSessions:10,async isRelevant(){const e=f();return"Apple_Terminal"===v.terminal?!e.optionAsMetaKeyInstalled:!e.shiftEnterKeyBindingInstalled}},{id:"shift-enter",content:async()=>"Apple_Terminal"===v.terminal?"Presiona Option+Enter para enviar un mensaje multilínea":"Presiona Shift+Enter para enviar un mensaje multilínea",cooldownSessions:10,async isRelevant(){const e=f();return Boolean(("Apple_Terminal"===v.terminal?e.optionAsMetaKeyInstalled:e.shiftEnterKeyBindingInstalled)&&e.numStartups>3)}},{id:"shift-enter-setup",content:async()=>"Apple_Terminal"===v.terminal?"Ejecuta /terminal-setup para habilitar Option+Enter para nuevas líneas":"Ejecuta /terminal-setup para habilitar Shift+Enter para nuevas líneas",cooldownSessions:10,async isRelevant(){if(!i())return!1;const e=f();return!("Apple_Terminal"===v.terminal?e.optionAsMetaKeyInstalled:e.shiftEnterKeyBindingInstalled)}},{id:"memory-command",content:async()=>"Usa /memory para ver y gestionar la memoria de Context",cooldownSessions:15,isRelevant:async()=>f().memoryUsageCount<=0},{id:"theme-command",content:async()=>"Usa /theme para cambiar el tema de color",cooldownSessions:20,isRelevant:async()=>!0},{id:"colorterm-truecolor",content:async()=>"Prueba a configurar la variable de entorno COLORTERM=truecolor para obtener colores más vivos",cooldownSessions:30,isRelevant:async()=>!process.env.COLORTERM&&e.level<3},{id:"powershell-tool-env",content:async()=>"Configura CONTEXT_CODE_USE_POWERSHELL_TOOL=1 para habilitar la herramienta PowerShell (vista previa)",cooldownSessions:10,isRelevant:async()=>"windows"===x()&&void 0===process.env.CONTEXT_CODE_USE_POWERSHELL_TOOL&&void 0===process.env.CLAUDE_CODE_USE_POWERSHELL_TOOL},{id:"status-line",content:async()=>"Usa /statusline para configurar una línea de estado personalizada que se mostrará debajo del cuadro de entrada",cooldownSessions:25,isRelevant:async()=>void 0===t().statusLine},{id:"prompt-queue",content:async()=>"Pulsa Enter para poner en cola mensajes adicionales mientras Context está trabajando.",cooldownSessions:5,isRelevant:async()=>f().promptQueueUseCount<=3},{id:"enter-to-steer-in-relatime",content:async()=>"Envía mensajes a Context mientras trabaja para guiarlo en tiempo real",cooldownSessions:20,isRelevant:async()=>!0},{id:"todo-list",content:async()=>"Pide a Context que cree una lista de tareas cuando trabajes en tareas complejas para seguir el progreso",cooldownSessions:20,isRelevant:async()=>!0},{id:"vscode-command-install",content:async()=>`Abre la Paleta de Comandos (Cmd+Shift+P) y ejecuta "Shell Command: Install '${"vscode"===v.terminal?"code":v.terminal}' command in PATH" para habilitar la integración con el IDE`,cooldownSessions:0,async isRelevant(){if(!j())return!1;if("macos"!==x())return!1;switch(v.terminal){case"vscode":return!await E();case"cursor":return!await b();case"windsurf":return!await P();default:return!1}}},{id:"ide-upsell-external-terminal",content:async()=>"Conecta Context a tu IDE · /ide",cooldownSessions:4,async isRelevant(){if(C())return!1;if(0!==(await R()).length)return!1;return(await w()).length>0}},{id:"install-github-app",content:async()=>"Ejecuta /install-github-app para etiquetar a @claude directamente desde tus issues y PRs de Github",cooldownSessions:10,isRelevant:async()=>!f().githubActionSetupCount},{id:"install-slack-app",content:async()=>"Ejecuta /install-slack-app para usar Context en Slack",cooldownSessions:10,isRelevant:async()=>!f().slackAppInstallCount},{id:"permissions",content:async()=>"Usa /permissions para pre-aprobar y pre-denegar herramientas de bash, edición y MCP",cooldownSessions:10,isRelevant:async()=>f().numStartups>10},{id:"drag-and-drop-images",content:async()=>"¿Sabías que puedes arrastrar y soltar archivos de imagen en tu terminal?",cooldownSessions:10,isRelevant:async()=>!v.isSSH()},{id:"paste-images-mac",content:async()=>"Pega imágenes en Context Code usando control+v (¡no cmd+v!)",cooldownSessions:10,isRelevant:async()=>"macos"===x()},{id:"double-esc",content:async()=>"Pulsa dos veces esc para rebobinar la conversación a un punto anterior en el tiempo",cooldownSessions:10,isRelevant:async()=>!a()},{id:"double-esc-code-restore",content:async()=>"Pulsa dos veces esc para rebobinar el código y/o la conversación a un punto anterior en el tiempo",cooldownSessions:10,isRelevant:async()=>a()},{id:"continue",content:async()=>"Ejecuta context --continue o context --resume para reanudar una conversación",cooldownSessions:10,isRelevant:async()=>!0},{id:"rename-conversation",content:async()=>"Nombra tus conversaciones con /rename para encontrarlas fácilmente en /resume más tarde",cooldownSessions:15,isRelevant:async()=>M()&&f().numStartups>10},{id:"custom-commands",content:async()=>"Crea habilidades añadiendo archivos .md a .context/skills/ en tu proyecto o ~/.context/skills/ para habilidades globales",cooldownSessions:15,isRelevant:async()=>f().numStartups>10},{id:"shift-tab",content:async()=>"ant"===process.env.USER_TYPE?`Pulsa ${d("chat:cycleMode","Chat","shift+tab")} para alternar entre el modo predeterminado y el modo automático`:`Pulsa ${d("chat:cycleMode","Chat","shift+tab")} para alternar entre el modo predeterminado, el modo de edición con auto-aceptación y el modo de planificación`,cooldownSessions:10,isRelevant:async()=>!0},{id:"image-paste",content:async()=>`Usa ${d("chat:imagePaste","Chat","ctrl+v")} para pegar imágenes desde tu portapapeles`,cooldownSessions:20,isRelevant:async()=>!0},{id:"custom-agents",content:async()=>"Usa /agents para optimizar tareas específicas. Ej: Arquitecto de Software, Escritor de Código, Revisor de Código",cooldownSessions:15,isRelevant:async()=>f().numStartups>5},{id:"agent-flag",content:async()=>"Usa --agent <nombre_agente> para iniciar directamente una conversación con un subagente",cooldownSessions:15,isRelevant:async()=>f().numStartups>5},{id:"desktop-app",content:async()=>"Ejecuta Context Code local o remotamente usando la aplicación de escritorio: clau.de/desktop",cooldownSessions:15,isRelevant:async()=>"linux"!==x()},{id:"desktop-shortcut",content:async e=>`Continúa tu sesión en Context Code Desktop con ${l("suggestion",e.theme)("/desktop")}`,cooldownSessions:15,isRelevant:async()=>!!r().enable_shortcut_tip&&("darwin"===process.platform||"win32"===process.platform&&"x64"===process.arch)},{id:"web-app",content:async()=>"Ejecuta tareas en la nube mientras sigues programando localmente · clau.de/web",cooldownSessions:15,isRelevant:async()=>!0},{id:"mobile-app",content:async()=>"/mobile para usar Context Code desde la aplicación de Context en tu teléfono",cooldownSessions:15,isRelevant:async()=>!0},{id:"opusplan-mode-reminder",content:async()=>`Tu modelo predeterminado es Opus Plan Mode. Presiona ${d("chat:cycleMode","Chat","shift+tab")} dos veces para activar el Modo Plan y planificar con Context Opus.`,cooldownSessions:2,async isRelevant(){if("ant"===process.env.USER_TYPE)return!1;const e=f(),n="opusplan"===T(),a=e.lastPlanModeUse?(Date.now()-e.lastPlanModeUse)/864e5:1/0;return n&&a>3}},{id:"frontend-design-plugin",content:async e=>`¿Trabajando con HTML/CSS? Instala el plugin frontend-design:\n${l("suggestion",e.theme)(`/plugin install frontend-design@${O}`)}`,cooldownSessions:3,isRelevant:async e=>isMarketplacePluginRelevant("frontend-design",e,{filePath:/\.(html|css|htm)$/i})},{id:"vercel-plugin",content:async e=>`¿Trabajando con Vercel? Instala el plugin vercel:\n${l("suggestion",e.theme)(`/plugin install vercel@${O}`)}`,cooldownSessions:3,isRelevant:async e=>isMarketplacePluginRelevant("vercel",e,{filePath:/(?:^|[/\\])vercel\.json$/i,cli:["vercel"]})},{id:"effort-high-nudge",content:async e=>{const n=l("suggestion",e.theme)("/effort high");return"copy_b"===L("tengu_tide_elm","off")?`Usa ${n} para obtener mejores respuestas directas. Context lo analiza profundamente primero.`:`¿Trabajando en algo difícil? ${n} ofrece mejores respuestas iniciales`},cooldownSessions:3,isRelevant:async()=>{if(!p())return!1;if(!g(_()))return!1;if(void 0!==o("policySettings")?.effortLevel)return!1;if(void 0!==y())return!1;const e=s().effortLevel;return"high"!==e&&"max"!==e&&"off"!==L("tengu_tide_elm","off")}},{id:"subagent-fanout-nudge",content:async e=>{const n=l("suggestion",e.theme);return"copy_b"===L("tengu_tern_alloy","off")?`Para tareas grandes, pide a Context que ${n("use subagentes")}. Trabajan en paralelo y mantienen limpia la conversación principal.`:`Di ${n('"fan out subagents"')} y Context enviará un equipo. Cada uno profundiza para que no se escape nada.`},cooldownSessions:3,isRelevant:async()=>!!p()&&"off"!==L("tengu_tern_alloy","off")},{id:"loop-command-nudge",content:async e=>{const n=l("suggestion",e.theme);return"copy_b"===L("tengu_timber_lark","off")?`Usa ${n("/loop 5m check the deploy")} para ejecutar cualquier prompt de forma programada. Configúralo y olvídate.`:`${n("/loop")} ejecuta cualquier prompt de forma recurrente. Ideal para monitorear despliegues o estados de PR.`},cooldownSessions:3,isRelevant:async()=>!!p()&&(!!u()&&"off"!==L("tengu_timber_lark","off"))},{id:"guest-passes",content:async e=>{const n=l("context",e.theme),a=z();return a?`Comparte Context Code y gana ${n(q(a))} de uso extra · ${n("/passes")}`:`Tienes pases de invitado gratuitos para compartir · ${n("/passes")}`},cooldownSessions:3,isRelevant:async()=>{if(f().hasVisitedPasses)return!1;const{eligible:e}=I();return e}},{id:"overage-credit",content:async e=>{const n=l("context",e.theme),a=D(),s=a?A(a):null;return s?`${n(`${s} en uso extra, por nuestra cuenta`)} · aplicaciones de terceros · ${n("/extra-usage")}`:""},cooldownSessions:3,isRelevant:async()=>c()},{id:"feedback-command",content:async()=>"¡Usa /feedback para ayudarnos a mejorar!",cooldownSessions:15,async isRelevant(){if("ant"===process.env.USER_TYPE)return!1;return f().numStartups>5}}],B="ant"===process.env.USER_TYPE?[{id:"important-claudemd",content:async()=>'[ANT-ONLY] Use "IMPORTANT:" prefix for must-follow CLAUDE.md rules',cooldownSessions:30,isRelevant:async()=>!0},{id:"skillify",content:async()=>"[ANT-ONLY] Use /skillify at the end of a workflow to turn it into a reusable skill",cooldownSessions:15,isRelevant:async()=>!0}]:[];export async function getRelevantTips(e){const n=s().spinnerTipsOverride,a=function(){const e=s().spinnerTipsOverride;return e?.tips?.length?e.tips.map((e,n)=>({id:`custom-tip-${n}`,content:async()=>e,cooldownSessions:0,isRelevant:async()=>!0})):[]}();if(n?.excludeDefault&&a.length>0)return a;const t=[...Y,...B],o=await Promise.all(t.map(n=>n.isRelevant(e)));return[...t.filter((e,n)=>o[n]).filter(e=>H(e.id)>=e.cooldownSessions),...a]}
@@ -1 +1 @@
1
- import{E_TOOL_USE_SUMMARY_GENERATION_FAILED as t}from"../../constants/errorIds.js";import{toError as n}from"../../utils/errors.js";import{logError as e}from"../../utils/log.js";import{jsonStringify as s}from"../../utils/slowOperations.js";import{asSystemPrompt as o}from"../../utils/systemPromptType.js";import{queryHaiku as r}from"../api/claude.js";export async function generateToolUseSummary({tools:s,signal:a,isNonInteractiveSession:i,lastAssistantText:l}){if(0===s.length)return null;try{const t=s.map(t=>{const n=truncateJson(t.input,300),e=truncateJson(t.output,300);return`Tool: ${t.name}\nInput: ${n}\nOutput: ${e}`}).join("\n\n"),n=l?`User's intent (from assistant's last message): ${l.slice(0,200)}\n\n`:"",e=await r({systemPrompt:o(["Write a short summary label describing what these tool calls accomplished. It appears as a single-line row in a mobile app and truncates around 30 characters, so think git-commit-subject, not sentence.\n\nKeep the verb in past tense and the most distinctive noun. Drop articles, connectors, and long location context first.\n\nExamples:\n- Searched in auth/\n- Fixed NPE in UserService\n- Created signup endpoint\n- Read config.json\n- Ran failing tests"]),userPrompt:`${n}Tools completed:\n\n${t}\n\nLabel:`,signal:a,options:{querySource:"tool_use_summary_generation",enablePromptCaching:!0,agents:[],isNonInteractiveSession:i,hasAppendSystemPrompt:!1,mcpTools:[]}});return e.message.content.filter(t=>"text"===t.type).map(t=>"text"===t.type?t.text:"").join("").trim()||null}catch(s){const o=n(s);return o.cause={errorId:t},e(o),null}}function truncateJson(t,n){try{const e=s(t);return e.length<=n?e:e.slice(0,n-3)+"..."}catch{return"[unable to serialize]"}}
1
+ const e=30;export async function generateToolUseSummary({tools:n}){return 0===n.length?null:1===n.length?function(n){const o=t[n.name]??n.name,i=pickNoun(n.input);return clipTo(i?`${o} ${i}`:o,e)}(n[0]):function(n){const o=new Map,i=new Map;let s="";for(const e of n){const n=t[e.name]??e.name;if(o.set(n,(o.get(n)??0)+1),!s&&"object"==typeof e.input&&e.input){const t=e.input,n=t.file_path??t.path??t.filepath;"string"==typeof n&&(s=dirname(n))}if(!i.has(n)){const t=pickNoun(e.input);t&&i.set(n,basename(t))}}if(1===o.size){const[t,n]=[...o.entries()][0],r=i.get(t);if("Read"===t||"Wrote"===t||"Edited"===t){return clipTo(`${t} ${n} file${n>1?"s":""}${s?` in ${s}`:""}`,e)}return clipTo(r?`${t} ${r}`:`${t} ${n}`,e)}const r=[];for(const[e,t]of o){const n=i.get(e);if(n?r.push(`${e} ${n}`):r.push(`${e} ${t}`),3===r.length&&o.size>3){r.push(`+${o.size-3} more`);break}}return clipTo(r.join(", "),e)}(n)}const t={Read:"Read",Write:"Wrote",Edit:"Edited",Glob:"Listed paths in",Grep:"Searched in",Bash:"Ran",PowerShell:"Ran",WebFetch:"Fetched",WebSearch:"Searched",Agent:"Delegated to",TodoWrite:"Updated todos",ToolSearch:"Discovered tools",NotebookEdit:"Edited notebook",ListMcpResources:"Listed MCP resources",mcp__database__query:"Queried DB",mcp__database__list_tables:"Listed DB tables",mcp__database__describe_table:"Described table"};function pickNoun(e){if(!e||"object"!=typeof e)return"";const t=e;for(const e of["file_path","filepath","path","filePath"])if("string"==typeof t[e])return t[e];return"string"==typeof t.command?t.command:"string"==typeof t.pattern?t.pattern:"string"==typeof t.query?t.query:"string"==typeof t.url?t.url:"string"==typeof t.resource?t.resource:""}function clipTo(e,t){const n=e.trim();return n?n.length<=t?n:n.slice(0,t-1)+"…":null}function dirname(e){const t=Math.max(e.lastIndexOf("/"),e.lastIndexOf("\\"));return-1===t?"":e.slice(0,t)}function basename(e){const t=Math.max(e.lastIndexOf("/"),e.lastIndexOf("\\"));return-1===t?e:e.slice(t+1)}
@@ -1 +1 @@
1
- import{jsx as e,jsxs as t}from"react/jsx-runtime";import{c as r}from"react/compiler-runtime";import n from"figures";import{Markdown as i}from"../../components/Markdown.js";import{BLACK_CIRCLE as o}from"../../constants/figures.js";import{Box as l,Text as m}from"../../ink.js";import{getDisplayPath as s}from"../../utils/file.js";import{formatFileSize as c}from"../../utils/format.js";import{formatBriefTimestamp as a}from"../../utils/formatBriefTimestamp.js";export function renderToolUseMessage(){return""}export function renderToolResultMessage(r,n,s){const c=(r.attachments?.length??0)>0;if(!r.message&&!c)return null;if(s?.isTranscriptMode)return t(l,{flexDirection:"row",marginTop:1,children:[e(l,{minWidth:2,children:e(m,{color:"text",children:o})}),t(l,{flexDirection:"column",children:[r.message?e(i,{children:r.message}):null,e(AttachmentList,{attachments:r.attachments})]})]});if(s?.isBriefOnly){const n=r.sentAt?a(r.sentAt):"";return t(l,{flexDirection:"column",marginTop:1,paddingLeft:2,children:[t(l,{flexDirection:"row",children:[e(m,{color:"briefLabelClaude",children:"Claude"}),n?t(m,{dimColor:!0,children:[" ",n]}):null]}),t(l,{flexDirection:"column",children:[r.message?e(i,{children:r.message}):null,e(AttachmentList,{attachments:r.attachments})]})]})}return t(l,{flexDirection:"row",marginTop:1,children:[e(l,{minWidth:2}),t(l,{flexDirection:"column",children:[r.message?e(i,{children:r.message}):null,e(AttachmentList,{attachments:r.attachments})]})]})}export function AttachmentList(t){const n=r(4),{attachments:i}=t;if(!i||0===i.length)return null;let o,m;return n[0]!==i?(o=i.map(_temp),n[0]=i,n[1]=o):o=n[1],n[2]!==o?(m=e(l,{flexDirection:"column",marginTop:1,children:o}),n[2]=o,n[3]=m):m=n[3],m}function _temp(r){return t(l,{flexDirection:"row",children:[t(m,{dimColor:!0,children:[n.pointerSmall," ",r.isImage?"[image]":"[file]"," "]}),e(m,{children:s(r.path)}),t(m,{dimColor:!0,children:[" (",c(r.size),")"]})]},r.path)}
1
+ import{jsx as e,jsxs as t}from"react/jsx-runtime";import{c as r}from"react/compiler-runtime";import n from"figures";import{Markdown as i}from"../../components/Markdown.js";import{BLACK_CIRCLE as o}from"../../constants/figures.js";import{Box as l,Text as m}from"../../ink.js";import{getDisplayPath as s}from"../../utils/file.js";import{formatFileSize as c}from"../../utils/format.js";import{formatBriefTimestamp as a}from"../../utils/formatBriefTimestamp.js";export function renderToolUseMessage(){return""}export function renderToolResultMessage(r,n,s){const c=(r.attachments?.length??0)>0;if(!r.message&&!c)return null;if(s?.isTranscriptMode)return t(l,{flexDirection:"row",marginTop:1,children:[e(l,{minWidth:2,children:e(m,{color:"text",children:o})}),t(l,{flexDirection:"column",children:[r.message?e(i,{children:r.message}):null,e(AttachmentList,{attachments:r.attachments})]})]});if(s?.isBriefOnly){const n=r.sentAt?a(r.sentAt):"";return t(l,{flexDirection:"column",marginTop:1,paddingLeft:2,children:[t(l,{flexDirection:"row",children:[e(m,{color:"briefLabelContext",children:"Claude"}),n?t(m,{dimColor:!0,children:[" ",n]}):null]}),t(l,{flexDirection:"column",children:[r.message?e(i,{children:r.message}):null,e(AttachmentList,{attachments:r.attachments})]})]})}return t(l,{flexDirection:"row",marginTop:1,children:[e(l,{minWidth:2}),t(l,{flexDirection:"column",children:[r.message?e(i,{children:r.message}):null,e(AttachmentList,{attachments:r.attachments})]})]})}export function AttachmentList(t){const n=r(4),{attachments:i}=t;if(!i||0===i.length)return null;let o,m;return n[0]!==i?(o=i.map(_temp),n[0]=i,n[1]=o):o=n[1],n[2]!==o?(m=e(l,{flexDirection:"column",marginTop:1,children:o}),n[2]=o,n[3]=m):m=n[3],m}function _temp(r){return t(l,{flexDirection:"row",children:[t(m,{dimColor:!0,children:[n.pointerSmall," ",r.isImage?"[image]":"[file]"," "]}),e(m,{children:s(r.path)}),t(m,{dimColor:!0,children:[" (",c(r.size),")"]})]},r.path)}
@@ -1 +1 @@
1
- import{spawn as r}from"child_process";import{join as o}from"path";import{getCwd as e}from"../cwd.js";import{COMPUTER_CONTROL_MCP_SERVER_NAME as t}from"./common.js";export function runComputerControlMcpServer(){const c=o(e(),"CLI","src","utils","computerControlMcp","server"),p="win32"===process.platform?"py":"python3",s=r(p,["-m","computer_control_mcp.server"],{cwd:c,stdio:"inherit",env:{...process.env,PYTHONPATH:o(c,"src")}});return s.on("error",r=>{console.error(`Failed to start ${t} server:`,r)}),s}
1
+ import{spawn as r}from"child_process";import{join as o,dirname as e}from"path";import{fileURLToPath as t}from"url";import{COMPUTER_CONTROL_MCP_SERVER_NAME as m}from"./common.js";import{ensureUvAvailable as p}from"../sembleMcp/setup.js";export function runComputerControlMcpServer(){const s=e(t(import.meta.url)),c=o(s,"server"),n=p(),i=r(n,["run","--project",c,"-m","computer_control_mcp.server"],{cwd:c,stdio:"inherit",env:{...process.env,PYTHONPATH:o(c,"src"),UV_SKIP_WHEEL_FILENAME_CHECK:"1"}});return i.on("error",r=>{console.error(`Failed to start ${m} server:`,r)}),i}
@@ -0,0 +1,18 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
3
+
4
+ # Python files
5
+ *.py text diff=python
6
+
7
+ # Documentation
8
+ *.md text
9
+ *.rst text
10
+ *.txt text
11
+
12
+ # Exclude files from exporting
13
+ .gitattributes export-ignore
14
+ .gitignore export-ignore
15
+ .github export-ignore
16
+ .pytest_cache export-ignore
17
+ __pycache__ export-ignore
18
+ *.pyc export-ignore
@@ -0,0 +1,25 @@
1
+ # Use a lightweight Python base image
2
+ FROM python:3.12-slim
3
+
4
+ # Set environment variables for Python
5
+ ENV PYTHONDONTWRITEBYTECODE=1 \
6
+ PYTHONUNBUFFERED=1
7
+
8
+ # Set working directory
9
+ WORKDIR /app
10
+
11
+ # Copy dependency file(s)
12
+ COPY pyproject.toml .
13
+ COPY src/ src/
14
+ COPY README.md README.md
15
+
16
+ # Install build backend (Hatchling)
17
+ RUN pip install --upgrade pip && \
18
+ pip install hatchling && \
19
+ pip install -e .
20
+
21
+ # Copy any additional files (e.g. configs, CLI, entrypoints)
22
+ COPY . .
23
+
24
+ # Default command (can be overridden)
25
+ CMD ["python", "-m", "computer_control_mcp"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 AB498 <abcd49800@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ include README.md
2
+ include LICENSE
3
+ include pyproject.toml
4
+ recursive-include src *
5
+ recursive-include tests *
6
+ recursive-include docs *
7
+ global-exclude *.pyc
8
+ global-exclude __pycache__
9
+ global-exclude *.so
10
+ global-exclude .DS_Store
@@ -0,0 +1,193 @@
1
+ # Computer Control MCP
2
+
3
+ ### MCP server that provides computer control capabilities, like mouse, keyboard, OCR, etc. using PyAutoGUI, RapidOCR, ONNXRuntime. Similar to 'computer-use' by Anthropic. With Zero External Dependencies.
4
+
5
+ <div align="center" style="text-align:center;font-family: monospace; display: flex; align-items: center; justify-content: center; width: 100%; gap: 10px">
6
+ <a href="https://nextjs-boilerplate-ashy-nine-64.vercel.app/demo-computer-control"><img
7
+ src="https://komarev.com/ghpvc/?username=AB498&label=DEMO&style=for-the-badge&color=CC0000" /></a>
8
+ <a href="https://discord.gg/ZeeqSBpjU2"><img
9
+ src="https://img.shields.io/discord/1095854826786668545?style=for-the-badge&color=0000CC" alt="Discord"></a>
10
+ <a href="https://img.shields.io/badge/License-MIT-yellow.svg"><img
11
+ src="https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge&color=00CC00" alt="License: MIT"></a>
12
+ <a href="https://pypi.org/project/computer-control-mcp"><img
13
+ src="https://img.shields.io/pypi/v/computer-control-mcp?style=for-the-badge" alt="PyPi"></a>
14
+ </div>
15
+
16
+ ---
17
+
18
+ ![MCP Computer Control Demo](https://github.com/AB498/computer-control-mcp/blob/main/demonstration.gif?raw=true)
19
+
20
+ ## Quick Usage (MCP Setup Using `uvx`)
21
+
22
+ ***Note:** Running `uvx computer-control-mcp@latest` for the first time will download python dependencies (around 70MB) which may take some time. Recommended to run this in a terminal before using it as MCP. Subsequent runs will be instant.*
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "computer-control-mcp": {
28
+ "command": "uvx",
29
+ "args": ["computer-control-mcp@latest"]
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ OR install globally with `pip`:
36
+ ```bash
37
+ pip install computer-control-mcp
38
+ ```
39
+ Then run the server with:
40
+ ```bash
41
+ computer-control-mcp # instead of uvx computer-control-mcp, so you can use the latest version, also you can `uv cache clean` to clear the cache and `uvx` again to use latest version.
42
+ ```
43
+
44
+ ## Features
45
+
46
+ - Control mouse movements and clicks
47
+ - Type text at the current cursor position
48
+ - Take screenshots of the entire screen or specific windows with optional saving to downloads directory
49
+ - Extract text from screenshots using OCR (Optical Character Recognition)
50
+ - List and activate windows
51
+ - Press keyboard keys
52
+ - Drag and drop operations
53
+ - Enhanced screenshot capture for GPU-accelerated windows (Windows only)
54
+
55
+ ## Note on GPU-accelerated Windows
56
+
57
+ Traditional screenshot methods like GDI/PrintWindow fail to capture GPU-accelerated windows, resulting in black screens. This impacts games, media players, Electron apps, browsers with GPU acceleration, streaming software, and CAD tools. Use WGC through `take_screenshot` tool's flag or `ENV` variable
58
+
59
+ ## Configuration
60
+
61
+ ### Custom Screenshot Directory
62
+
63
+ By default, screenshots are saved to the OS downloads directory. You can customize this by setting the `COMPUTER_CONTROL_MCP_SCREENSHOT_DIR` environment variable:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "computer-control-mcp": {
69
+ "command": "uvx",
70
+ "args": ["computer-control-mcp@latest"],
71
+ "env": {
72
+ "COMPUTER_CONTROL_MCP_SCREENSHOT_DIR": "C:\\Users\\YourName\\Pictures\\Screenshots"
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ Or set it system-wide:
80
+ ```bash
81
+ # Windows (PowerShell)
82
+ $env:COMPUTER_CONTROL_MCP_SCREENSHOT_DIR = "C:\Users\YourName\Pictures\Screenshots"
83
+
84
+ # macOS/Linux
85
+ export COMPUTER_CONTROL_MCP_SCREENSHOT_DIR="/home/yourname/Pictures/Screenshots"
86
+ ```
87
+
88
+ If the specified directory doesn't exist, the server will fall back to the default downloads directory.
89
+
90
+ ### Automatic WGC for Specific Windows
91
+
92
+ You can configure the system to automatically use Windows Graphics Capture (WGC) for specific windows by setting the `COMPUTER_CONTROL_MCP_WGC_PATTERNS` environment variable. This variable should contain comma-separated patterns that match window titles:
93
+
94
+ ```json
95
+ {
96
+ "mcpServers": {
97
+ "computer-control-mcp": {
98
+ "command": "uvx",
99
+ "args": ["computer-control-mcp@latest"],
100
+ "env": {
101
+ "COMPUTER_CONTROL_MCP_WGC_PATTERNS": "obs, discord, game, steam"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ Or set it system-wide:
109
+ ```bash
110
+ # Windows (PowerShell)
111
+ $env:COMPUTER_CONTROL_MCP_WGC_PATTERNS = "obs, discord, game, steam"
112
+
113
+ # macOS/Linux
114
+ export COMPUTER_CONTROL_MCP_WGC_PATTERNS="obs, discord, game, steam"
115
+ ```
116
+
117
+ When this variable is set, any window whose title contains any of the specified patterns will automatically use WGC for screenshot capture, eliminating black screens for GPU-accelerated applications.
118
+
119
+ ## Available Tools
120
+
121
+ ### Mouse Control
122
+ - `click_screen(x: int, y: int)`: Click at specified screen coordinates
123
+ - `move_mouse(x: int, y: int)`: Move mouse cursor to specified coordinates
124
+ - `drag_mouse(from_x: int, from_y: int, to_x: int, to_y: int, duration: float = 0.5)`: Drag mouse from one position to another
125
+ - `mouse_down(button: str = "left")`: Hold down a mouse button ('left', 'right', 'middle')
126
+ - `mouse_up(button: str = "left")`: Release a mouse button ('left', 'right', 'middle')
127
+
128
+ ### Keyboard Control
129
+ - `type_text(text: str)`: Type the specified text at current cursor position
130
+ - `press_key(key: str)`: Press a specified keyboard key
131
+ - `key_down(key: str)`: Hold down a specific keyboard key until released
132
+ - `key_up(key: str)`: Release a specific keyboard key
133
+ - `press_keys(keys: Union[str, List[Union[str, List[str]]]])`: Press keyboard keys (supports single keys, sequences, and combinations)
134
+
135
+ ### Screen and Window Management
136
+ - `take_screenshot(title_pattern: str = None, use_regex: bool = False, threshold: int = 60, scale_percent_for_ocr: int = None, save_to_downloads: bool = False, use_wgc: bool = False)`: Capture screen or window
137
+ - `take_screenshot_with_ocr(title_pattern: str = None, use_regex: bool = False, threshold: int = 10, scale_percent_for_ocr: int = None, save_to_downloads: bool = False)`: Extract adn return text with coordinates using OCR from screen or window
138
+ - `get_screen_size()`: Get current screen resolution
139
+ - `list_windows()`: List all open windows
140
+ - `activate_window(title_pattern: str, use_regex: bool = False, threshold: int = 60)`: Bring specified window to foreground
141
+ - `wait_milliseconds(milliseconds: int)`: Wait for a specified number of milliseconds
142
+
143
+ ## Development
144
+
145
+ ### Setting up the Development Environment
146
+
147
+ ```bash
148
+ # Clone the repository
149
+ git clone https://github.com/AB498/computer-control-mcp.git
150
+ cd computer-control-mcp
151
+
152
+ # Build/Run:
153
+
154
+ # 1. Install in development mode | Meaning that your edits to source code will be reflected in the installed package.
155
+ pip install -e .
156
+
157
+ # Then Start server | This is equivalent to `uvx computer-control-mcp@latest` just the local code is used
158
+ computer-control-mcp
159
+
160
+ # -- OR --
161
+
162
+ # 2. Build after `pip install hatch` | This needs version increment in orer to reflect code changes
163
+ hatch build
164
+
165
+ # Windows
166
+ $latest = Get-ChildItem .\dist\*.whl | Sort-Object LastWriteTime -Descending | Select-Object -First 1
167
+ pip install $latest.FullName --upgrade
168
+
169
+ # Non-windows
170
+ pip install dist/*.whl --upgrade
171
+
172
+ # Run
173
+ computer-control-mcp
174
+ ```
175
+
176
+ ### Running Tests
177
+
178
+ ```bash
179
+ python -m pytest
180
+ ```
181
+
182
+ ## API Reference
183
+
184
+ See the [API Reference](docs/api.md) for detailed information about the available functions and classes.
185
+
186
+ ## License
187
+
188
+ MIT
189
+
190
+ ## For more information or help
191
+
192
+ - [Email (abcd49800@gmail.com)](mailto:abcd49800@gmail.com)
193
+ - [Discord (CodePlayground)](https://discord.gg/ZeeqSBpjU2)
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "computer-control-mcp"
7
+ version = "0.3.10"
8
+ description = "MCP server that provides computer control capabilities, like mouse, keyboard, OCR, etc. using PyAutoGUI, RapidOCR, ONNXRuntime. Similar to 'computer-use' by Anthropic. With Zero External Dependencies."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "AB498", email = "abcd49800@gmail.com"}]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Software Development :: Libraries",
20
+ "Topic :: Utilities"
21
+ ]
22
+ dependencies = [
23
+ "pyautogui==0.9.54",
24
+ "mcp[cli]==1.13.0",
25
+ "pillow==11.3.0",
26
+ "pygetwindow==0.0.9; sys_platform=='win32'",
27
+ "pywinctl==0.4.1",
28
+ "fuzzywuzzy==0.18.0",
29
+ "rapidocr==3.3.1",
30
+ "onnxruntime==1.22.0",
31
+ "rapidocr_onnxruntime==1.2.3",
32
+ "opencv-python==4.12.0.88",
33
+ "python-Levenshtein>=0.20.9",
34
+ "mss>=7.0.0",
35
+ "windows-capture>=1.0.0; sys_platform=='win32'"
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/AB498/computer-control-mcp"
40
+ Issues = "https://github.com/AB498/computer-control-mcp/issues"
41
+ Documentation = "https://github.com/AB498/computer-control-mcp#readme"
42
+
43
+ [project.scripts]
44
+ computer-control-mcp = "computer_control_mcp.cli:main"
45
+ computer-control-mcp-server = "computer_control_mcp.server:main"
46
+
47
+ [tool.hatch.build]
48
+ sources = ["src"]
49
+ packages = ["src/computer_control_mcp"]
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/computer_control_mcp"]
@@ -0,0 +1,13 @@
1
+ # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2
+
3
+ startCommand:
4
+ type: stdio
5
+ configSchema:
6
+ # JSON Schema defining the configuration options for the MCP.
7
+ type: object
8
+ description: Empty config
9
+ commandFunction:
10
+ # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
11
+ |-
12
+ (config) => ({ command: 'python', args: ['src/computer_control_mcp/core.py'] })
13
+ exampleConfig: {}
@@ -0,0 +1,12 @@
1
+ # Computer Control MCP Source Code
2
+
3
+ This directory contains the source code for the Computer Control MCP package.
4
+
5
+ ## Structure
6
+
7
+ - `computer_control_mcp/`: Main package directory
8
+ - `__init__.py`: Package initialization
9
+ - `__main__.py`: Entry point for running as a module
10
+ - `core.py`: Core functionality
11
+ - `cli.py`: Command-line interface
12
+ - `gui.py`: Graphical user interface for testing
@@ -0,0 +1,11 @@
1
+ """
2
+ Computer Control MCP - Python package for computer control via MCP.
3
+
4
+ This package provides computer control capabilities using PyAutoGUI through a
5
+ Model Context Protocol (MCP) server.
6
+ """
7
+
8
+ from computer_control_mcp.core import mcp, main
9
+
10
+ __version__ = "0.1.2"
11
+ __all__ = ["mcp", "main"]
@@ -0,0 +1,21 @@
1
+ """
2
+ Entry point for running the Computer Control MCP as a module.
3
+
4
+ This module serves as the main entry point for the package.
5
+ When executed directly (e.g., with `python -m computer_control_mcp`),
6
+ it will run the CLI interface.
7
+
8
+ For CLI functionality, use:
9
+ computer-control-mcp <command>
10
+ python -m computer_control_mcp <command>
11
+ """
12
+
13
+ from computer_control_mcp.cli import main as cli_main
14
+
15
+ def main():
16
+ """Main entry point for the package."""
17
+ # Run the CLI when the module is executed directly
18
+ cli_main()
19
+
20
+ if __name__ == "__main__":
21
+ main()