WindsurfAdapter.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. import { BaseAdapter } from "./BaseAdapter.js";
  2. import type {
  3. OpenAgent,
  4. ConversionResult,
  5. ToolCapabilities,
  6. ToolConfig,
  7. AgentFrontmatter,
  8. } from "../types.js";
  9. /**
  10. * Windsurf adapter for converting between OpenAgents Control and Windsurf formats.
  11. *
  12. * Windsurf uses:
  13. * - `.windsurf/config.json` for main configuration
  14. * - `.windsurf/agents/*.json` for individual agents
  15. * - `.windsurf/context/` for context files
  16. *
  17. * **Key Features**:
  18. * - ✅ Multiple agents support
  19. * - ⚠️ Partial permissions (binary on/off, not granular)
  20. * - ⚠️ Limited temperature support (maps to creativity)
  21. * - ⚠️ Partial skills support
  22. * - ❌ No hooks support
  23. *
  24. * @see https://windsurf.ai/docs
  25. */
  26. export class WindsurfAdapter extends BaseAdapter {
  27. readonly name = "windsurf";
  28. readonly displayName = "Windsurf";
  29. constructor() {
  30. super();
  31. }
  32. /**
  33. * Get the config path for Windsurf.
  34. */
  35. getConfigPath(): string {
  36. return ".windsurf/";
  37. }
  38. // ============================================================================
  39. // CONVERSION METHODS
  40. // ============================================================================
  41. /**
  42. * Convert Windsurf format TO OpenAgents Control format.
  43. *
  44. * Expects JSON content from either:
  45. * - `.windsurf/config.json` (main config)
  46. * - `.windsurf/agents/{name}.json` (agent config)
  47. *
  48. * @param source - Windsurf JSON config content
  49. * @returns OpenAgent object
  50. */
  51. toOAC(source: string): Promise<OpenAgent> {
  52. const config = this.safeParseJSON(source, "windsurf-config.json");
  53. if (!config || typeof config !== "object") {
  54. return Promise.reject(new Error("Invalid Windsurf config format"));
  55. }
  56. const windsurfConfig = config as Record<string, unknown>;
  57. // Build agent frontmatter
  58. const frontmatter: AgentFrontmatter = {
  59. name: String(windsurfConfig.name || "windsurf-agent"),
  60. description: String(
  61. windsurfConfig.description || "Agent imported from Windsurf"
  62. ),
  63. mode: windsurfConfig.type === "subagent" ? "subagent" : "primary",
  64. model: this.mapWindsurfModelToOAC(windsurfConfig.model as string),
  65. tools: this.parseWindsurfTools(windsurfConfig.tools),
  66. };
  67. // Map creativity to temperature (approximate)
  68. if (windsurfConfig.creativity !== undefined) {
  69. frontmatter.temperature = this.mapCreativityToTemperature(
  70. windsurfConfig.creativity as string | number
  71. );
  72. }
  73. // Validate category is one of the valid AgentCategory values
  74. const validCategories = ["core", "development", "content", "data", "product", "learning", "meta", "specialist"] as const;
  75. const categoryStr = String(windsurfConfig.category || "core");
  76. const category = validCategories.includes(categoryStr as never) ? (categoryStr as typeof validCategories[number]) : "core";
  77. return Promise.resolve({
  78. frontmatter,
  79. metadata: {
  80. name: frontmatter.name,
  81. category,
  82. type: frontmatter.mode === "subagent" ? "subagent" : "agent",
  83. },
  84. systemPrompt: String(windsurfConfig.systemPrompt || windsurfConfig.prompt || ""),
  85. contexts: this.parseWindsurfContexts(windsurfConfig.contexts),
  86. });
  87. }
  88. /**
  89. * Convert FROM OpenAgents Control format to Windsurf format.
  90. *
  91. * Generates:
  92. * - `.windsurf/agents/{name}.json` for each agent
  93. * - Context file references
  94. *
  95. * @param agent - OpenAgent to convert
  96. * @returns ConversionResult with generated files and warnings
  97. */
  98. fromOAC(agent: OpenAgent): Promise<ConversionResult> {
  99. const warnings: string[] = [];
  100. const configs: ToolConfig[] = [];
  101. // Validate conversion
  102. const validationWarnings = this.validateConversion(agent);
  103. warnings.push(...validationWarnings);
  104. // Check for unsupported features
  105. if (agent.frontmatter.hooks && agent.frontmatter.hooks.length > 0) {
  106. warnings.push(
  107. this.unsupportedFeatureWarning(
  108. "hooks",
  109. `${agent.frontmatter.hooks.length} hooks`
  110. )
  111. );
  112. warnings.push("❌ Windsurf does not support hooks - behavioral rules will be lost");
  113. }
  114. if (agent.frontmatter.maxSteps !== undefined) {
  115. warnings.push(
  116. this.unsupportedFeatureWarning("maxSteps", agent.frontmatter.maxSteps)
  117. );
  118. }
  119. // Check for granular permissions
  120. if (agent.frontmatter.permission) {
  121. const hasGranular = Object.values(agent.frontmatter.permission).some(
  122. (perm) => typeof perm === "object" && !Array.isArray(perm)
  123. );
  124. if (hasGranular) {
  125. warnings.push(
  126. this.degradedFeatureWarning(
  127. "granular permissions",
  128. "allow/deny/ask per path",
  129. "binary on/off per tool"
  130. )
  131. );
  132. }
  133. }
  134. // Check for skills
  135. if (agent.frontmatter.skills && agent.frontmatter.skills.length > 0) {
  136. warnings.push(
  137. this.degradedFeatureWarning(
  138. "skills",
  139. "full Skills system",
  140. "basic context references"
  141. )
  142. );
  143. }
  144. // Generate Windsurf agent config
  145. const windsurfConfig = this.generateWindsurfConfig(agent, warnings);
  146. // Determine output path based on agent mode
  147. const fileName =
  148. agent.frontmatter.mode === "subagent"
  149. ? `.windsurf/agents/${agent.frontmatter.name}.json`
  150. : ".windsurf/config.json";
  151. configs.push({
  152. fileName,
  153. content: JSON.stringify(windsurfConfig, null, 2),
  154. encoding: "utf-8",
  155. });
  156. // Generate context file references if present
  157. if (agent.contexts && agent.contexts.length > 0) {
  158. warnings.push(
  159. `💡 ${agent.contexts.length} context file(s) referenced - ensure they exist in .windsurf/context/`
  160. );
  161. }
  162. return Promise.resolve(this.createSuccessResult(configs, warnings));
  163. }
  164. /**
  165. * Get Windsurf capabilities.
  166. */
  167. getCapabilities(): ToolCapabilities {
  168. return {
  169. name: this.name,
  170. displayName: this.displayName,
  171. supportsMultipleAgents: true, // ✅ Supports .windsurf/agents/
  172. supportsSkills: true, // ⚠️ Partial - basic context support
  173. supportsHooks: false, // ❌ Not supported
  174. supportsGranularPermissions: false, // ⚠️ Binary only
  175. supportsContexts: true, // ✅ .windsurf/context/
  176. supportsCustomModels: true, // ✅ Model selection
  177. supportsTemperature: true, // ⚠️ Via creativity setting
  178. supportsMaxSteps: false, // ❌ Not supported
  179. configFormat: "json",
  180. outputStructure: "directory",
  181. notes: [
  182. "Multiple agents supported via .windsurf/agents/",
  183. "Permissions are binary (on/off) - granular rules degraded",
  184. "Temperature maps to/from creativity setting (low/medium/high)",
  185. "No hooks support - behavioral rules will be lost",
  186. "Skills map to basic context file references",
  187. "Priority levels: only high/low (medium/critical → degraded)",
  188. ],
  189. };
  190. }
  191. /**
  192. * Validate if an agent can be converted with full fidelity.
  193. */
  194. validateConversion(agent: OpenAgent): string[] {
  195. const warnings: string[] = [];
  196. if (!agent.frontmatter.name) {
  197. warnings.push("⚠️ Agent name is required for Windsurf");
  198. }
  199. if (!agent.frontmatter.description) {
  200. warnings.push("⚠️ Agent description recommended for Windsurf");
  201. }
  202. return warnings;
  203. }
  204. // ============================================================================
  205. // PARSING HELPERS (toOAC)
  206. // ============================================================================
  207. /**
  208. * Parse Windsurf tools to OAC ToolAccess.
  209. */
  210. private parseWindsurfTools(
  211. tools: unknown
  212. ): Record<string, boolean> | undefined {
  213. if (!tools) return undefined;
  214. const toolAccess: Record<string, boolean> = {};
  215. if (typeof tools === "object" && !Array.isArray(tools)) {
  216. // Parse object: { read: true, write: false }
  217. const toolsObj = tools as Record<string, unknown>;
  218. for (const [tool, enabled] of Object.entries(toolsObj)) {
  219. toolAccess[tool.toLowerCase()] = Boolean(enabled);
  220. }
  221. } else if (Array.isArray(tools)) {
  222. // Parse array: ["read", "write", "bash"]
  223. tools.forEach((tool) => {
  224. const toolName = String(tool).toLowerCase();
  225. if (toolName) {
  226. toolAccess[toolName] = true;
  227. }
  228. });
  229. }
  230. return Object.keys(toolAccess).length > 0 ? toolAccess : undefined;
  231. }
  232. /**
  233. * Parse Windsurf contexts to OAC format.
  234. */
  235. private parseWindsurfContexts(
  236. contexts: unknown
  237. ): Array<{ path: string; priority?: "critical" | "high" | "medium" | "low"; description?: string }> {
  238. if (!contexts || !Array.isArray(contexts)) return [];
  239. return contexts.map((ctx) => {
  240. if (typeof ctx === "string") {
  241. return { path: ctx };
  242. }
  243. if (typeof ctx === "object" && ctx !== null) {
  244. const ctxObj = ctx as Record<string, unknown>;
  245. const priority = ctxObj.priority ? String(ctxObj.priority).toLowerCase() : undefined;
  246. return {
  247. path: String(ctxObj.path || ""),
  248. priority: this.normalizeOACPriority(priority),
  249. description: ctxObj.description ? String(ctxObj.description) : undefined,
  250. };
  251. }
  252. return { path: "" };
  253. }).filter((ctx) => ctx.path);
  254. }
  255. /**
  256. * Normalize priority to OAC valid values.
  257. */
  258. private normalizeOACPriority(
  259. priority?: string
  260. ): "critical" | "high" | "medium" | "low" | undefined {
  261. if (!priority) return undefined;
  262. const normalized = priority.toLowerCase();
  263. if (
  264. normalized === "critical" ||
  265. normalized === "high" ||
  266. normalized === "medium" ||
  267. normalized === "low"
  268. ) {
  269. return normalized;
  270. }
  271. // Default to medium if invalid
  272. return "medium";
  273. }
  274. /**
  275. * Map Windsurf creativity setting to OAC temperature.
  276. */
  277. private mapCreativityToTemperature(creativity: string | number): number {
  278. if (typeof creativity === "number") {
  279. return creativity; // Already numeric
  280. }
  281. const creativityMap: Record<string, number> = {
  282. low: 0.3,
  283. medium: 0.7,
  284. high: 1.0,
  285. balanced: 0.5,
  286. };
  287. return creativityMap[creativity.toLowerCase()] || 0.7;
  288. }
  289. // ============================================================================
  290. // GENERATION HELPERS (fromOAC)
  291. // ============================================================================
  292. /**
  293. * Generate Windsurf config from OpenAgent.
  294. */
  295. private generateWindsurfConfig(
  296. agent: OpenAgent,
  297. warnings: string[]
  298. ): Record<string, unknown> {
  299. const config: Record<string, unknown> = {
  300. name: agent.frontmatter.name,
  301. description: agent.frontmatter.description,
  302. type: agent.frontmatter.mode === "subagent" ? "subagent" : "primary",
  303. systemPrompt: agent.systemPrompt,
  304. };
  305. // Model mapping
  306. if (agent.frontmatter.model) {
  307. config.model = this.mapOACModelToWindsurf(agent.frontmatter.model);
  308. }
  309. // Tools mapping
  310. if (agent.frontmatter.tools) {
  311. config.tools = this.mapOACToolsToWindsurf(agent.frontmatter.tools);
  312. }
  313. // Temperature → creativity
  314. if (agent.frontmatter.temperature !== undefined) {
  315. config.creativity = this.mapTemperatureToCreativity(
  316. agent.frontmatter.temperature
  317. );
  318. }
  319. // Category
  320. if (agent.metadata?.category) {
  321. config.category = agent.metadata.category;
  322. }
  323. // Contexts
  324. if (agent.contexts && agent.contexts.length > 0) {
  325. config.contexts = agent.contexts.map((ctx) => ({
  326. path: ctx.path,
  327. priority: this.mapOACPriorityToWindsurf(ctx.priority || "medium"),
  328. description: ctx.description,
  329. }));
  330. }
  331. // Skills → context references
  332. if (agent.frontmatter.skills && agent.frontmatter.skills.length > 0) {
  333. const skillContexts = agent.frontmatter.skills.map((skill) => {
  334. const skillName = typeof skill === "string" ? skill : skill.name;
  335. return {
  336. path: `.windsurf/context/${skillName}.md`,
  337. priority: "medium",
  338. description: `Skill: ${skillName}`,
  339. };
  340. });
  341. if (!config.contexts) {
  342. config.contexts = skillContexts;
  343. } else {
  344. (config.contexts as Array<unknown>).push(...skillContexts);
  345. }
  346. }
  347. // Permissions (simplified)
  348. if (agent.frontmatter.permission) {
  349. config.permissions = this.mapOACPermissionsToWindsurf(
  350. agent.frontmatter.permission,
  351. warnings
  352. );
  353. }
  354. return config;
  355. }
  356. // ============================================================================
  357. // MAPPING HELPERS
  358. // ============================================================================
  359. /**
  360. * Map Windsurf model ID to OAC model ID.
  361. */
  362. private mapWindsurfModelToOAC(model?: string): string | undefined {
  363. if (!model) return undefined;
  364. const modelMap: Record<string, string> = {
  365. "claude-4-sonnet": "claude-sonnet-4",
  366. "claude-4-opus": "claude-opus-4",
  367. "claude-4-haiku": "claude-haiku-4",
  368. "gpt-4": "gpt-4",
  369. "gpt-4-turbo": "gpt-4-turbo",
  370. "gpt-4o": "gpt-4o",
  371. };
  372. return modelMap[model] || model;
  373. }
  374. /**
  375. * Map OAC model ID to Windsurf model ID.
  376. */
  377. private mapOACModelToWindsurf(model: string): string {
  378. const modelMap: Record<string, string> = {
  379. "claude-sonnet-4": "claude-4-sonnet",
  380. "claude-opus-4": "claude-4-opus",
  381. "claude-haiku-4": "claude-4-haiku",
  382. "gpt-4": "gpt-4",
  383. "gpt-4-turbo": "gpt-4-turbo",
  384. "gpt-4o": "gpt-4o",
  385. };
  386. return modelMap[model] || "claude-4-sonnet"; // Default
  387. }
  388. /**
  389. * Map OAC ToolAccess to Windsurf tools object.
  390. */
  391. private mapOACToolsToWindsurf(
  392. tools: Record<string, boolean>
  393. ): Record<string, boolean> {
  394. // Windsurf uses binary on/off for tools
  395. const windsurfTools: Record<string, boolean> = {};
  396. for (const [tool, enabled] of Object.entries(tools)) {
  397. windsurfTools[tool] = Boolean(enabled);
  398. }
  399. return windsurfTools;
  400. }
  401. /**
  402. * Map OAC temperature to Windsurf creativity.
  403. */
  404. private mapTemperatureToCreativity(temperature: number): string {
  405. if (temperature <= 0.4) return "low";
  406. if (temperature <= 0.8) return "medium";
  407. return "high";
  408. }
  409. /**
  410. * Map OAC priority to Windsurf priority.
  411. */
  412. private mapOACPriorityToWindsurf(priority: string): string {
  413. const priorityMap: Record<string, string> = {
  414. critical: "high",
  415. high: "high",
  416. medium: "low",
  417. low: "low",
  418. };
  419. return priorityMap[priority.toLowerCase()] || "low";
  420. }
  421. /**
  422. * Map OAC granular permissions to Windsurf binary permissions.
  423. */
  424. private mapOACPermissionsToWindsurf(
  425. permissions: Record<string, unknown>,
  426. warnings: string[]
  427. ): Record<string, boolean> {
  428. const windsurfPerms: Record<string, boolean> = {};
  429. // Analyze each permission and convert to binary
  430. for (const [tool, perm] of Object.entries(permissions)) {
  431. if (typeof perm === "boolean") {
  432. windsurfPerms[tool] = perm;
  433. } else if (perm === "allow") {
  434. windsurfPerms[tool] = true;
  435. } else if (perm === "deny") {
  436. windsurfPerms[tool] = false;
  437. } else if (perm === "ask") {
  438. // "ask" → default to false (cautious approach)
  439. windsurfPerms[tool] = false;
  440. warnings.push(
  441. `⚠️ Permission "ask" for ${tool} degraded to false (deny). Windsurf only supports binary on/off.`
  442. );
  443. } else if (typeof perm === "object" && perm !== null) {
  444. // Granular permissions - default to true if any allow exists
  445. const permObj = perm as Record<string, unknown>;
  446. const hasAllow = permObj.allow !== undefined;
  447. windsurfPerms[tool] = hasAllow;
  448. } else {
  449. windsurfPerms[tool] = false;
  450. }
  451. }
  452. return windsurfPerms;
  453. }
  454. }