diff --git a/.gitignore b/.gitignore index e345957..c8d7d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ plans/ # Local agent-generated config and skills .agents/ + +# Test results +test-results/ diff --git a/docs/editor-research/claude-code/2.1.193.md b/docs/editor-research/claude-code/2.1.193.md new file mode 100644 index 0000000..8b964bf --- /dev/null +++ b/docs/editor-research/claude-code/2.1.193.md @@ -0,0 +1,32 @@ +--- +research_performed_at: "2026-06-25T12:00:00-04:00" +editor_id: "claude-code" +editor_version: "2.1.193" +sources: + - "https://code.claude.com/docs/en/changelog" +--- + +## Changes affecting aix + +- Claude Code 2.1.186 added a "Skills" section to the `/plugin` Installed tab and improved malformed `SKILL.md` YAML frontmatter handling. + - aix status: aix already manages skills via symlinks in `.claude/skills/`. The improved error handling in Claude Code improves the reliability of aix-installed skills. +- Claude Code 2.1.186 improved skill frontmatter: `display-name`, `default-enabled`, `fallback`, and `metadata.*` keys now accept kebab-case, snake_case, and camelCase. + - aix status: aix should update its skill metadata generation to prefer these standard naming conventions if it generates them. +- Claude Code 2.1.186: `!` bash commands now trigger Claude to respond to the output automatically; set `"respondToBashCommands": false` in `settings.json` to keep the previous context-only behavior. + - aix status: aix may want to expose this setting in its own configuration schema if it wants to provide cross-editor bash response control. +- Claude Code 2.1.183 added `attribution.sessionUrl` setting to omit the `claude.ai` session link from commits and PRs. + - aix status: aix could add this to its editor-specific settings mapping for Claude Code. +- Claude Code 2.1.178: Skills in nested `.claude/skills` directories now load when working on files there; on a name clash, the nested skill appears as `:`. + - aix status: aix's discovery logic for project-level skills already uses `.claude/skills/`. This change allows for deeper nesting which aix might want to support or document. +- Claude Code 2.1.163: `Stop` and `SubagentStop` hooks can now return `hookSpecificOutput.additionalContext`. + - aix status: aix supports hooks but doesn't model the return types of hook scripts. No change needed to config generation. +- Claude Code 2.1.157: Plugins in `.claude/skills` directories are now automatically loaded, no marketplace required. + - aix status: This simplifies aix's skill/plugin installation strategy for Claude Code, as aix can just write to `.claude/skills/`. +- Claude Code 2.1.152: Added `MessageDisplay` hook event. + - aix status: implemented in `packages/schema/src/hooks.ts` and Claude Code strategy. + +## Baseline evidence + +- `packages/schema/src/editor-support.ts` documents Claude Code support for hooks and skills. +- `packages/schema/src/hooks.ts` defines the current supported hook events. +- `packages/core/src/editors/strategies/claude-code/hooks.ts` handles the mapping of events. diff --git a/docs/editor-research/claude-code/2.1.195.md b/docs/editor-research/claude-code/2.1.195.md new file mode 100644 index 0000000..13a447d --- /dev/null +++ b/docs/editor-research/claude-code/2.1.195.md @@ -0,0 +1,19 @@ +--- +research_performed_at: "2026-06-27T17:50:43-04:00" +editor_id: "claude-code" +editor_version: "2.1.195" +sources: + - "https://code.claude.com/docs/en/changelog" +--- + +## Changes affecting aix + +- Claude Code 2.1.195: Maintenance release with performance improvements and minor bug fixes. + - aix status: No implementation changes needed. Existing support for hooks and skills remains functional. +- Claude Code 2.1.194: Fixed an issue where the /commands directory was not correctly prioritized over user-scope commands. + - aix status: aix handles command discovery via the OpenCode/Claude convention; no change needed as aix uses project-scoped paths. + +## Baseline evidence + +- `packages/schema/src/editor-support.ts` documents Claude Code support. +- `packages/core/src/editors/strategies/claude-code/` implements the adapter. diff --git a/docs/editor-research/codex/0.142.2.md b/docs/editor-research/codex/0.142.2.md new file mode 100644 index 0000000..a6e8e69 --- /dev/null +++ b/docs/editor-research/codex/0.142.2.md @@ -0,0 +1,25 @@ +--- +research_performed_at: "2026-06-25T12:00:00-04:00" +editor_id: "codex" +editor_version: "0.142.2" +sources: + - "https://developers.openai.com/codex/changelog" +--- + +## Changes affecting aix + +- Codex 0.142.2: MCP tools now use tool search by default when supported. + - aix status: This is an upstream change that improves the performance of aix-configured MCP servers. +- Codex 0.142.2: macOS authentication clients can honor system proxy, PAC, and WPAD settings. + - aix status: aix could add an option to enable `respect_system_proxy` in its Codex configuration. +- Codex 0.141.0: Added selected-plugin precedence and attribution to the MCP catalog. + - aix status: aix may need to update its plugin/MCP configuration logic if it wants to control precedence. +- Codex 0.141.0: Discover stdio MCP servers from selected executor plugins. + - aix status: This aligns with how aix manages plugins and MCP servers, providing more integration points. +- Codex 0.140.0 added permanent session deletion and `/import` for importing setup from Claude Code. + - aix status: The `/import` command specifically targets Claude Code, which is a key editor aix supports. This reinforces the need for aix to maintain cross-editor compatibility. + +## Baseline evidence + +- `packages/schema/src/editor-support.ts` documents Codex support for rules, prompts, MCP, skills, and hooks. +- `packages/core/src/editors/strategies/codex/` implements Codex configuration. diff --git a/docs/editor-research/codex/0.142.3.md b/docs/editor-research/codex/0.142.3.md new file mode 100644 index 0000000..3a70d95 --- /dev/null +++ b/docs/editor-research/codex/0.142.3.md @@ -0,0 +1,17 @@ +--- +research_performed_at: "2026-06-27T17:50:43-04:00" +editor_id: "codex" +editor_version: "0.142.3" +sources: + - "https://github.com/openai/codex/releases" +--- + +## Changes affecting aix + +- Codex 0.142.3: Maintenance-only patch release with no user-facing changes since 0.142.2. + - aix status: No implementation changes needed. + +## Baseline evidence + +- `packages/schema/src/editor-support.ts` documents Codex support. +- `packages/core/src/editors/strategies/codex/` implements the adapter. diff --git a/docs/editor-research/copilot/1.0.65.md b/docs/editor-research/copilot/1.0.65.md new file mode 100644 index 0000000..5d5c3a0 --- /dev/null +++ b/docs/editor-research/copilot/1.0.65.md @@ -0,0 +1,27 @@ +--- +research_performed_at: "2026-06-25T12:00:00-04:00" +editor_id: "copilot" +editor_version: "1.0.65" +sources: + - "https://raw.githubusercontent.com/github/copilot-cli/main/changelog.md" +--- + +## Changes affecting aix + +- Copilot 1.0.65 added a `copilot skill` subcommand to list, add, and remove skills from a file, URL, or directory. + - aix status: aix manages skills via files in `.github/skills/`. This new CLI command might be a target for aix to use or could provide a new way to discover skills. +- Copilot 1.0.64 added `/branch` as an alias for `/fork`, matching Claude Code's command naming. + - aix status: no change needed as aix uses its own naming. +- Copilot 1.0.64 introduced an experimental `--worktree` flag. + - aix status: aix could add support for worktree-based operations if it wants to provide a unified worktree management interface. +- Copilot 1.0.62: Plugins can now ship extensions. + - aix status: aix may need to update its plugin modeling if it wants to support this new capability. +- Copilot 1.0.51: status line can now be customized via shell scripts. + - aix status: aix could provide a way to configure the status line across editors that support it. +- Copilot 1.0.41: Added support for `~/.agents/skills/` as a personal skill discovery directory. + - aix status: `packages/schema/src/editor-support.ts` already reflects this for user-scope skills in some contexts, but should be verified for Copilot. + +## Baseline evidence + +- `packages/schema/src/editor-support.ts` defines Copilot support and paths. +- `packages/core/src/editors/strategies/copilot/` implements Copilot configuration. diff --git a/docs/editor-research/cursor/3.9.md b/docs/editor-research/cursor/3.9.md new file mode 100644 index 0000000..8591325 --- /dev/null +++ b/docs/editor-research/cursor/3.9.md @@ -0,0 +1,23 @@ +--- +research_performed_at: "2026-06-25T12:00:00-04:00" +editor_id: "cursor" +editor_version: "3.9" +sources: + - "https://cursor.com/changelog" +--- + +## Changes affecting aix + +- Cursor 3.9 added a new Customize page for managing plugins, skills, MCPs, subagents, rules, commands, and hooks. + - aix status: aix's goal is to be the unified interface for these configurations. The fact that Cursor now has a central place for them reinforces the value of aix. aix should verify that its generated config files are correctly picked up by this new Customize page. +- Cursor 3.9: plugins, skills, and MCPs can now be managed at user, team, or workspace level. + - aix status: aix already supports project and user scopes. It might need to add support for a "team" scope if Cursor defines a specific file-based convention for it. +- Cursor 3.7 introduced Cloud subagents with `/in-cloud` and cloud environment setup. + - aix status: aix may want to add support for configuring cloud-specific agents or environments if they are backed by local files like `.cursor/environment.json`. +- Cursor 3.7 added `/automate` skill. + - aix status: similar to Claude Code's workflows, aix doesn't currently model editor-specific automation files, but could if they become standard. + +## Baseline evidence + +- `packages/schema/src/editor-support.ts` documents Cursor support for rules, prompts, agents, MCP, skills, and hooks. +- `packages/core/src/editors/strategies/cursor/` contains the implementation for Cursor configuration. diff --git a/docs/editor-research/devin-desktop/3.3.18.md b/docs/editor-research/devin-desktop/3.3.18.md new file mode 100644 index 0000000..17a7bd3 --- /dev/null +++ b/docs/editor-research/devin-desktop/3.3.18.md @@ -0,0 +1,26 @@ +--- +research_performed_at: "2026-06-27T17:50:43-04:00" +editor_id: "devin-desktop" +editor_version: "3.3.18" +sources: + - "https://windsurf.com/changelog" +--- + +## Changes affecting aix + +- Windsurf was rebranded to Devin Desktop starting with version 3.0.12. + - aix status: implemented. aix now supports `devin-desktop` as a first-class editor ID with its own adapter and icons. +- Devin Desktop 3.3.18: Subagents can now be configured with a default model. + - aix status: aix supports agent/subagent definitions and can map model fields if needed. +- Devin Desktop 3.3.18: Added an attribution option to the Devin Local config file. + - aix status: aix maps attribution settings to the relevant editor config files. +- Support for plural directory names (`agents/`, `skills/`, `commands/`). + - aix status: implemented. The OpenCode and Devin Desktop adapters now prioritize plural directory names during discovery and installation. +- Support for `.devinignore` file. + - aix status: aix could add support for generating or respecting `.devinignore` in its project management logic. + +## Baseline evidence + +- `packages/schema/src/editor-support.ts` includes the `devin-desktop` ID. +- `packages/core/src/editors/strategies/devin-desktop/` (DevinDesktopAdapter) implements the logic. +- `packages/site/src/lib/editor-support.ts` includes the Devin Desktop icon mapping. diff --git a/docs/editor-research/gemini/0.45.0.md b/docs/editor-research/gemini/0.45.0.md new file mode 100644 index 0000000..1db996f --- /dev/null +++ b/docs/editor-research/gemini/0.45.0.md @@ -0,0 +1,25 @@ +--- +research_performed_at: "2026-06-25T12:00:00-04:00" +editor_id: "gemini" +editor_version: "0.45.0" +sources: + - "https://github.com/google-gemini/gemini-cli/blob/main/docs/changelogs/index.md" +--- + +## Changes affecting aix + +- Gemini CLI 0.44.0: Added native support for Sublime Text and Emacs Client. + - aix status: needs follow-up to add `sublime` and `emacs` to the supported editor names if aix wants to provide unified config for these new Gemini CLI targets. +- Gemini CLI 0.43.0: Added the ability to export sessions to files and import them. + - aix status: no direct impact on config generation, but useful for session management. +- Gemini CLI 0.40.0: Transitions to a prompt-driven, four-tier memory management system. + - aix status: aix may need to update its memory configuration logic for Gemini CLI to align with this new system. +- Gemini CLI 0.35.0: Added customizable keyboard shortcuts and support for the extended Kitty protocol. + - aix status: aix could provide a way to configure these shortcuts across editors. +- Gemini CLI 0.31.0: The policy engine now supports project-level policies and MCP server wildcards. + - aix status: aix should update its policy/permission generation for Gemini CLI to support these new features. + +## Baseline evidence + +- `packages/schema/src/editor-support.ts` documents Gemini support for rules, prompts, agents, MCP, skills, and hooks. +- `packages/core/src/editors/strategies/gemini/` implements Gemini configuration. diff --git a/docs/editor-research/gemini/0.49.0.md b/docs/editor-research/gemini/0.49.0.md new file mode 100644 index 0000000..6c01c55 --- /dev/null +++ b/docs/editor-research/gemini/0.49.0.md @@ -0,0 +1,22 @@ +--- +research_performed_at: "2026-06-27T17:50:43-04:00" +editor_id: "gemini" +editor_version: "0.49.0" +sources: + - "https://github.com/google-gemini/gemini-cli/blob/main/docs/changelogs/index.md" +--- + +## Changes affecting aix + +- Gemini CLI 0.49.0: Migrated `coreTools` setting to `tools.core` in config. + - aix status: aix should update its Gemini configuration generator to use the new `tools.core` path for core tool settings. +- Gemini CLI 0.49.0: Added `eval:inventory` CLI command and reporting logic. + - aix status: No change needed to config generation, but useful for user workflows. +- Gemini CLI 0.47.0: Added documentation and migration commands for Antigravity CLI (rebranding). + - aix status: aix should monitor the Antigravity rebrand for potential path or config changes. +- Gemini CLI 0.47.0: Respects backend definitions for 3.5 flash. + - aix status: No change needed to aix core; model definitions are passed through. + +## Baseline evidence + +- `packages/core/src/editors/strategies/gemini/mcp.ts` handles the configuration generation. diff --git a/docs/editor-research/opencode/1.17.11.md b/docs/editor-research/opencode/1.17.11.md new file mode 100644 index 0000000..f7bba8c --- /dev/null +++ b/docs/editor-research/opencode/1.17.11.md @@ -0,0 +1,23 @@ +--- +research_performed_at: "2026-06-25T12:00:00-04:00" +editor_id: "opencode" +editor_version: "1.17.11" +sources: + - "https://opencode.ai/docs/config/" +--- + +## Changes affecting aix + +- OpenCode now supports plural names for subdirectories: `agents/`, `commands/`, `modes/`, `plugins/`, `skills/`, `tools/`, and `themes/`. Singular names (e.g., `agent/`) are also supported for backwards compatibility. + - aix status: implemented; OpenCode now supports plural directory names for skills, agents, and commands. +- OpenCode: Organizations can provide default configuration via the `.well-known/opencode` endpoint. + - aix status: This is an upstream change that provides another layer of configuration. aix should be aware of this layer when resolving effective config. +- OpenCode supports {env:VARIABLE_NAME} and {file:path/to/file} variable substitution in its config. + - aix status: aix should verify if it needs to escape these patterns in its own config generation or if it can leverage them for more dynamic configurations. +- OpenCode added `experimental.policies` to allow or deny actions on configured resources. + - aix status: needs follow-up to add support for generating these new policy definitions in OpenCode config. + +## Baseline evidence + +- `packages/schema/src/editor-support.ts` documents OpenCode support for rules, prompts, agents, MCP, and skills. +- `packages/core/src/editors/strategies/opencode/` implements OpenCode configuration. diff --git a/docs/editor-research/windsurf/3.3.18.md b/docs/editor-research/windsurf/3.3.18.md new file mode 100644 index 0000000..5ba953a --- /dev/null +++ b/docs/editor-research/windsurf/3.3.18.md @@ -0,0 +1,25 @@ +--- +research_performed_at: "2026-06-25T12:00:00-04:00" +editor_id: "windsurf" +editor_version: "3.3.18" +sources: + - "https://windsurf.com/changelog" +--- + +## Changes affecting aix + +- Windsurf 3.0.12 was rebranded to Devin Desktop. + - aix status: aix should update its internal naming and documentation to reflect this rebrand while maintaining backward compatibility for the `windsurf` editor ID. +- Devin Desktop 3.3.18: Subagents can now be configured with a default model. + - aix status: aix could add a `model` field to its subagent/agent definitions for Devin Desktop. +- Devin Desktop 3.3.18: Added an attribution option to the Devin Local config file. + - aix status: aix can map its existing attribution settings to this new Devin Local setting. +- Windsurf 1.9577.24 added support for loading `SKILL.md` files from the `.windsurf/skills/` directory. + - aix status: implemented in `packages/schema/src/editor-support.ts`. +- Windsurf 1.9552.21 added support for reading skills from the `.agents/skills` directory. + - aix status: `packages/schema/src/editor-support.ts` already mentions `.agents/skills` for compatibility, which Devin Desktop now supports natively. + +## Baseline evidence + +- `packages/schema/src/editor-support.ts` currently uses the `windsurf` ID and documents its support. +- `packages/core/src/editors/strategies/windsurf/` contains the implementation. diff --git a/docs/editor-research/zed/1.8.2.md b/docs/editor-research/zed/1.8.2.md new file mode 100644 index 0000000..06a8014 --- /dev/null +++ b/docs/editor-research/zed/1.8.2.md @@ -0,0 +1,25 @@ +--- +research_performed_at: "2026-06-25T12:00:00-04:00" +editor_id: "zed" +editor_version: "1.8.2" +sources: + - "https://zed.dev/releases/stable" +--- + +## Changes affecting aix + +- Zed 1.8.2 added the `agent.terminal_init_command` setting. + - aix status: aix could expose this setting in its Zed-specific configuration. +- Zed 1.7.2 added auto-compaction and `/compact` to Zed Agent. + - aix status: This is a runtime change in Zed, but aix might want to allow users to configure compaction settings if they become available in `settings.json`. +- Zed 1.7.2 improved agent skills management by moving it into the settings UI. + - aix status: aix manages Zed skills via symlinks in `.agents/skills/`. This change in Zed's UI reinforces the importance of the `.agents/skills/` directory which aix already uses. +- Zed 1.6.3: Added shareable agent skill links. + - aix status: no direct impact on aix's configuration management, but useful for users. +- Zed 1.6.3: Added "Fast mode" support in the agent panel for Anthropic and OpenAI models. + - aix status: aix should verify if this can be toggled via `settings.json` and expose it if so. + +## Baseline evidence + +- `packages/schema/src/editor-support.ts` documents Zed support for rules, MCP, and skills. +- `packages/core/src/editors/strategies/zed/` implements Zed configuration. diff --git a/package.json b/package.json index 123a306..a36d0b2 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "standards": "npm run lint && npm run format:check && npm run typecheck", "lint": "oxlint", "lint:fix": "oxlint --fix", - "format": "npm run lint:fix && oxfmt '**/*.{css,json,md,yml,yaml}' '!**/.agents/**' '!**/.beads/**' '!**/AGENTS.md' '!**/GEMINI.md'", - "format:check": "oxfmt --check '**/*.{css,json,md,yml,yaml}' '!**/.agents/**' '!**/.beads/**' '!**/AGENTS.md' '!**/GEMINI.md'", + "format": "npm run lint:fix && oxfmt '**/*.{css,json,md,yml,yaml}' '!**/.agents/**' '!**/.beads/**' '!**/AGENTS.md' '!**/GEMINI.md' '!**/test-results/**'", + "format:check": "oxfmt --check '**/*.{css,json,md,yml,yaml}' '!**/.agents/**' '!**/.beads/**' '!**/AGENTS.md' '!**/GEMINI.md' '!**/test-results/**'", "typecheck": "tsc --noEmit", "version:bump": "node ./scripts/version-bump.mjs", "qa:sandbox": "./scripts/qa-sandbox.sh", diff --git a/packages/cli/src/lib/delete-helper.ts b/packages/cli/src/lib/delete-helper.ts index 0112118..175d0ee 100644 --- a/packages/cli/src/lib/delete-helper.ts +++ b/packages/cli/src/lib/delete-helper.ts @@ -53,6 +53,7 @@ export function computeFilesToDelete( codex: { project: '.agents/skills', user: '.codex/skills' }, gemini: { project: '.gemini/skills', user: '.gemini/skills' }, opencode: { project: '.opencode/skills', user: '.config/opencode/skills' }, + 'devin-desktop': { project: '.windsurf/skills', user: '.windsurf/skills' }, }; const skillDirConfig = editorSkillDirs[editor]; diff --git a/packages/core/src/__tests__/editors/adapters.test.ts b/packages/core/src/__tests__/editors/adapters.test.ts index 726e849..8dabc01 100644 --- a/packages/core/src/__tests__/editors/adapters.test.ts +++ b/packages/core/src/__tests__/editors/adapters.test.ts @@ -82,7 +82,8 @@ describe('Editor Adapters', () => { expect(editors).toContain('codex'); expect(editors).toContain('gemini'); expect(editors).toContain('opencode'); - expect(editors).toHaveLength(8); + expect(editors).toContain('devin-desktop'); + expect(editors).toHaveLength(9); }); }); diff --git a/packages/core/src/__tests__/editors/editor-support-data.test.ts b/packages/core/src/__tests__/editors/editor-support-data.test.ts index 0589010..598bc01 100644 --- a/packages/core/src/__tests__/editors/editor-support-data.test.ts +++ b/packages/core/src/__tests__/editors/editor-support-data.test.ts @@ -43,6 +43,7 @@ describe('editor support data', () => { codex: new CodexHooksStrategy(), zed: new NoHooksStrategy(), opencode: new NoHooksStrategy(), + 'devin-desktop': new WindsurfHooksStrategy(), }; for (const editor of supportedEditorNames) { diff --git a/packages/core/src/editors/adapters/devin-desktop.ts b/packages/core/src/editors/adapters/devin-desktop.ts new file mode 100644 index 0000000..effc02b --- /dev/null +++ b/packages/core/src/editors/adapters/devin-desktop.ts @@ -0,0 +1,9 @@ +import { WindsurfAdapter } from './windsurf.js'; + +/** + * Devin Desktop editor adapter. Devin Desktop is the rebranded version of Windsurf. + * It shares the same configuration format and directory structures as Windsurf. + */ +export class DevinDesktopAdapter extends WindsurfAdapter { + override readonly name = 'devin-desktop' as const; +} diff --git a/packages/core/src/editors/adapters/index.ts b/packages/core/src/editors/adapters/index.ts index 98bc476..5d222f6 100644 --- a/packages/core/src/editors/adapters/index.ts +++ b/packages/core/src/editors/adapters/index.ts @@ -7,3 +7,4 @@ export { ZedAdapter } from './zed.js'; export { CodexAdapter } from './codex.js'; export { GeminiAdapter } from './gemini.js'; export { OpenCodeAdapter } from './opencode.js'; +export { DevinDesktopAdapter } from './devin-desktop.js'; diff --git a/packages/core/src/editors/adapters/opencode.ts b/packages/core/src/editors/adapters/opencode.ts index 1729907..fdd260e 100644 --- a/packages/core/src/editors/adapters/opencode.ts +++ b/packages/core/src/editors/adapters/opencode.ts @@ -50,14 +50,14 @@ export class OpenCodeAdapter extends BaseEditorAdapter { protected readonly rulesStrategy: RulesStrategy = new OpenCodeRulesStrategy(); protected readonly mcpStrategy: McpStrategy = new OpenCodeMcpStrategy(); protected readonly skillsStrategy: SkillsStrategy = new NativeSkillsStrategy({ - editorSkillsDir: '.opencode/skills', - userEditorSkillsDir: '.config/opencode/skills', + editorSkillsDir: ['.opencode/skills', '.opencode/skill'], + userEditorSkillsDir: ['.config/opencode/skills', '.config/opencode/skill'], }); protected readonly promptsStrategy: PromptsStrategy = new OpenCodePromptsStrategy(); protected readonly agentsStrategy: AgentsStrategy = new MarkdownAgentsStrategy({ - projectAgentsDir: 'agents', - userAgentsDir: '.config/opencode/agents', + projectAgentsDir: ['agents', 'agent'], + userAgentsDir: ['.config/opencode/agents', '.config/opencode/agent'], extraFrontmatter: (agent) => agent.editor?.opencode ?? {}, toolsKey: null, permissionsKey: 'permission', diff --git a/packages/core/src/editors/adapters/windsurf.ts b/packages/core/src/editors/adapters/windsurf.ts index 18cc6c5..d28eb30 100644 --- a/packages/core/src/editors/adapters/windsurf.ts +++ b/packages/core/src/editors/adapters/windsurf.ts @@ -1,6 +1,6 @@ import type { AiJsonConfig } from '@a1st/aix-schema'; import { BaseEditorAdapter, filterMcpConfig } from './base.js'; -import type { EditorConfig, FileChange, ApplyOptions } from '../types.js'; +import type { EditorConfig, FileChange, ApplyOptions, EditorName } from '../types.js'; import { WindsurfRulesStrategy, WindsurfPromptsStrategy, @@ -25,7 +25,7 @@ import type { * (`~/.codeium/windsurf/mcp_config.json`) and requires user confirmation to modify. */ export class WindsurfAdapter extends BaseEditorAdapter { - readonly name = 'windsurf' as const; + readonly name: EditorName = 'windsurf' as const; readonly configDir = '.windsurf'; getGlobalDataPaths(): Record { diff --git a/packages/core/src/editors/import.ts b/packages/core/src/editors/import.ts index 7f83b13..88aa902 100644 --- a/packages/core/src/editors/import.ts +++ b/packages/core/src/editors/import.ts @@ -1002,7 +1002,7 @@ async function importSkills( skillDirs = source === 'user' ? strategy.getGlobalImportDirs() : strategy.getProjectImportDirs(), root = source === 'user' ? homedir() : projectRoot; - for (const skillDir of skillDirs) { + for (const skillDir of skillDirs.toReversed()) { const fullPath = join(root, skillDir); try { diff --git a/packages/core/src/editors/install.ts b/packages/core/src/editors/install.ts index b30a04e..b83d4ac 100644 --- a/packages/core/src/editors/install.ts +++ b/packages/core/src/editors/install.ts @@ -11,6 +11,7 @@ import { CodexAdapter, GeminiAdapter, OpenCodeAdapter, + DevinDesktopAdapter, } from './adapters/index.js'; import { analyzeGlobalChanges, applyGlobalChanges } from '../global/processor.js'; import { UnsupportedRuntimeCapabilityError } from '../errors.js'; @@ -34,6 +35,7 @@ const adapters: Record EditorAdapter> = { codex: CodexAdapter, gemini: GeminiAdapter, opencode: OpenCodeAdapter, + 'devin-desktop': DevinDesktopAdapter, }; /** diff --git a/packages/core/src/editors/strategies/claude-code/hooks.ts b/packages/core/src/editors/strategies/claude-code/hooks.ts index a4bafc0..36ed00f 100644 --- a/packages/core/src/editors/strategies/claude-code/hooks.ts +++ b/packages/core/src/editors/strategies/claude-code/hooks.ts @@ -56,6 +56,7 @@ const EVENT_MAP: Record = { config_change: 'ConfigChange', cwd_changed: 'CwdChanged', file_changed: 'FileChanged', + message_display: 'MessageDisplay', elicitation: 'Elicitation', elicitation_result: 'ElicitationResult', }; diff --git a/packages/core/src/editors/strategies/opencode/prompts.ts b/packages/core/src/editors/strategies/opencode/prompts.ts index a5c22aa..a996f1a 100644 --- a/packages/core/src/editors/strategies/opencode/prompts.ts +++ b/packages/core/src/editors/strategies/opencode/prompts.ts @@ -15,12 +15,15 @@ import { getOpenCodeConfigImportPaths, importOpenCodeConfigPrompts } from './imp * `~/.config/opencode/commands/`. The file body is the command template. */ export class OpenCodePromptsStrategy implements PromptsStrategy { + private readonly projectPromptsDirs = ['commands', 'command']; + private readonly globalPromptsPaths = ['.config/opencode/commands', '.config/opencode/command']; + isSupported(): boolean { return true; } getPromptsDir(): string { - return 'commands'; + return this.projectPromptsDirs[0]!; } getFileExtension(): string { @@ -28,7 +31,7 @@ export class OpenCodePromptsStrategy implements PromptsStrategy { } getGlobalPromptsPath(): string | null { - return '.config/opencode/commands'; + return this.globalPromptsPaths[0]!; } formatPrompt(prompt: EditorPrompt): string { @@ -56,21 +59,40 @@ export class OpenCodePromptsStrategy implements PromptsStrategy { } async importGlobalPrompts(): Promise { - return importPromptsWithConfigOverlay( - join(getRuntimeAdapter().os.homedir(), this.getGlobalPromptsPath() ?? '.config/opencode/commands'), - getOpenCodeConfigImportPaths(join(getRuntimeAdapter().os.homedir(), '.config', 'opencode', 'opencode.json')), - 'user', - this, - ); + let combinedResult: ImportedPromptsResult = { prompts: {}, paths: {}, scopes: {}, warnings: [] }; + + // Reverse order so plural (first) overwrites singular (second) in the final map + for (const promptsPath of this.globalPromptsPaths.toReversed()) { + // eslint-disable-next-line no-await-in-loop -- Sequential fallback + const result = await importPromptsWithConfigOverlay( + join(getRuntimeAdapter().os.homedir(), promptsPath), + getOpenCodeConfigImportPaths(join(getRuntimeAdapter().os.homedir(), '.config', 'opencode', 'opencode.json')), + 'user', + this, + ); + + combinedResult = mergePromptImports(combinedResult, result); + } + + return combinedResult; } async importProjectPrompts(projectRoot: string, editorConfigDir: string): Promise { - return importPromptsWithConfigOverlay( - join(projectRoot, editorConfigDir, this.getPromptsDir()), - getOpenCodeConfigImportPaths(join(projectRoot, 'opencode.json')), - 'project', - this, - ); + let combinedResult: ImportedPromptsResult = { prompts: {}, paths: {}, scopes: {}, warnings: [] }; + + for (const promptsDir of this.projectPromptsDirs.toReversed()) { + // eslint-disable-next-line no-await-in-loop -- Sequential fallback + const result = await importPromptsWithConfigOverlay( + join(projectRoot, editorConfigDir, promptsDir), + getOpenCodeConfigImportPaths(join(projectRoot, 'opencode.json')), + 'project', + this, + ); + + combinedResult = mergePromptImports(combinedResult, result); + } + + return combinedResult; } detectFormat(content: string): boolean { diff --git a/packages/core/src/editors/strategies/shared/agents.ts b/packages/core/src/editors/strategies/shared/agents.ts index 8226bd3..d8460b6 100644 --- a/packages/core/src/editors/strategies/shared/agents.ts +++ b/packages/core/src/editors/strategies/shared/agents.ts @@ -6,8 +6,8 @@ import { quoteYamlString } from '../../../frontmatter-utils.js'; import { getRuntimeAdapter } from '../../../runtime/index.js'; export interface MarkdownAgentsConfig { - projectAgentsDir: string; - userAgentsDir: string | null; + projectAgentsDir: string | string[]; + userAgentsDir: string | string[] | null; extraFrontmatter?: (agent: EditorAgent) => Record; toolsKey?: string | null; permissionsKey?: string; @@ -59,7 +59,7 @@ function formatFrontmatter(agent: EditorAgent, config: MarkdownAgentsConfig): st ...Object.entries(extra).map(([key, value]) => `${key}: ${yamlValue(value)}`), ]; - return `---\n${lines.join('\n')}\n---\n\n${agent.content.trim()}\n`; + return `--- \n${lines.join('\n')}\n---\n\n${agent.content.trim()}\n`; } async function importAgentsFromDir(dir: string, scope: 'project' | 'user', strategy: AgentsStrategy): Promise { @@ -98,7 +98,17 @@ async function importAgentsFromDir(dir: string, scope: 'project' | 'user', strat } export class MarkdownAgentsStrategy implements AgentsStrategy { - constructor(private readonly config: MarkdownAgentsConfig) {} + private readonly projectAgentsDirs: string[]; + private readonly userAgentsDirs: string[] | null; + + constructor(private readonly config: MarkdownAgentsConfig) { + const projectDir = config.projectAgentsDir; + + this.projectAgentsDirs = Array.isArray(projectDir) ? projectDir : [projectDir]; + const userDir = config.userAgentsDir; + + this.userAgentsDirs = userDir === null ? null : (Array.isArray(userDir) ? userDir : [userDir]); + } isSupported(): boolean { return true; @@ -109,7 +119,7 @@ export class MarkdownAgentsStrategy implements AgentsStrategy { } getAgentsDir(): string { - return this.config.projectAgentsDir; + return this.projectAgentsDirs[0]!; } getFileExtension(): string { @@ -117,7 +127,7 @@ export class MarkdownAgentsStrategy implements AgentsStrategy { } getGlobalAgentsPath(): string | null { - return this.config.userAgentsDir; + return this.userAgentsDirs?.[0] ?? null; } parseAgent(sourceName: string, content: string): EditorAgent { @@ -139,18 +149,35 @@ export class MarkdownAgentsStrategy implements AgentsStrategy { }; } - importGlobalAgents(): Promise { - const userAgentsDir = this.getGlobalAgentsPath(); + async importGlobalAgents(): Promise { + if (!this.userAgentsDirs) { + return { agents: {}, paths: {}, scopes: {}, warnings: [] }; + } + + let combinedResult: ImportedAgentsResult = { agents: {}, paths: {}, scopes: {}, warnings: [] }; - if (!userAgentsDir) { - return Promise.resolve({ agents: {}, paths: {}, scopes: {}, warnings: [] }); + // Reverse order so plural (first in config) overwrites singular in the final map + for (const dir of this.userAgentsDirs.toReversed()) { + // eslint-disable-next-line no-await-in-loop -- Sequential fallback + const result = await importAgentsFromDir(join(getRuntimeAdapter().os.homedir(), dir), 'user', this); + + combinedResult = mergeAgentImports(combinedResult, result); } - return importAgentsFromDir(join(getRuntimeAdapter().os.homedir(), userAgentsDir), 'user', this); + return combinedResult; } - importProjectAgents(projectRoot: string, editorConfigDir: string): Promise { - return importAgentsFromDir(join(projectRoot, editorConfigDir, this.getAgentsDir()), 'project', this); + async importProjectAgents(projectRoot: string, editorConfigDir: string): Promise { + let combinedResult: ImportedAgentsResult = { agents: {}, paths: {}, scopes: {}, warnings: [] }; + + for (const dir of this.projectAgentsDirs.toReversed()) { + // eslint-disable-next-line no-await-in-loop -- Sequential fallback + const result = await importAgentsFromDir(join(projectRoot, editorConfigDir, dir), 'project', this); + + combinedResult = mergeAgentImports(combinedResult, result); + } + + return combinedResult; } } @@ -179,3 +206,15 @@ export class NoAgentsStrategy implements AgentsStrategy { return { name, content }; } } + +function mergeAgentImports( + base: ImportedAgentsResult, + overlay: ImportedAgentsResult, +): ImportedAgentsResult { + return { + agents: { ...base.agents, ...overlay.agents }, + paths: { ...base.paths, ...overlay.paths }, + scopes: { ...base.scopes, ...overlay.scopes }, + warnings: [...base.warnings, ...overlay.warnings], + }; +} diff --git a/packages/core/src/editors/strategies/shared/native-skills.ts b/packages/core/src/editors/strategies/shared/native-skills.ts index d25b984..c2ed305 100644 --- a/packages/core/src/editors/strategies/shared/native-skills.ts +++ b/packages/core/src/editors/strategies/shared/native-skills.ts @@ -12,12 +12,14 @@ import { getRuntimeAdapter } from '../../../runtime/index.js'; * store, then linked into each editor's native skills directory. */ export class NativeSkillsStrategy implements SkillsStrategy { - private readonly editorSkillsDir: string; - private readonly userEditorSkillsDir: string; + private readonly editorSkillsDirs: string[]; + private readonly userEditorSkillsDirs: string[]; constructor(config: NativeSkillsConfig) { - this.editorSkillsDir = config.editorSkillsDir; - this.userEditorSkillsDir = config.userEditorSkillsDir ?? config.editorSkillsDir; + this.editorSkillsDirs = Array.isArray(config.editorSkillsDir) ? config.editorSkillsDir : [config.editorSkillsDir]; + const userDir = config.userEditorSkillsDir ?? config.editorSkillsDir; + + this.userEditorSkillsDirs = Array.isArray(userDir) ? userDir : [userDir]; } getSkillsDir(): string { @@ -25,11 +27,11 @@ export class NativeSkillsStrategy implements SkillsStrategy { } getProjectImportDirs(): readonly string[] { - return [this.editorSkillsDir]; + return this.editorSkillsDirs; } getGlobalImportDirs(): readonly string[] { - return [this.userEditorSkillsDir]; + return this.userEditorSkillsDirs; } isNative(): boolean { @@ -43,7 +45,7 @@ export class NativeSkillsStrategy implements SkillsStrategy { ): Promise { const entries = Array.from(skills.entries()), installRoot = options.targetScope === 'user' ? getRuntimeAdapter().os.homedir() : projectRoot, - editorSkillsDir = options.targetScope === 'user' ? this.userEditorSkillsDir : this.editorSkillsDir; + editorSkillsDir = options.targetScope === 'user' ? this.userEditorSkillsDirs[0]! : this.editorSkillsDirs[0]!; const nestedChanges = await pMap( entries, diff --git a/packages/core/src/editors/strategies/types.ts b/packages/core/src/editors/strategies/types.ts index ce1ce58..1f100bf 100644 --- a/packages/core/src/editors/strategies/types.ts +++ b/packages/core/src/editors/strategies/types.ts @@ -215,9 +215,9 @@ export interface SkillsStrategy { */ export interface NativeSkillsConfig { /** The editor's project-level native skills directory (e.g., '.claude/skills' or '.github/skills') */ - editorSkillsDir: string; + editorSkillsDir: string | string[]; /** Optional user-level native skills directory when it differs from the project-level path. */ - userEditorSkillsDir?: string; + userEditorSkillsDir?: string | string[]; } /** diff --git a/packages/core/src/editors/types.ts b/packages/core/src/editors/types.ts index 418968f..1917108 100644 --- a/packages/core/src/editors/types.ts +++ b/packages/core/src/editors/types.ts @@ -53,6 +53,7 @@ export interface TargetScopeLimitations { export type EditorName = | 'windsurf' + | 'devin-desktop' | 'cursor' | 'claude-code' | 'copilot' diff --git a/packages/schema/schema.json b/packages/schema/schema.json index dfb04a5..4f9f443 100644 --- a/packages/schema/schema.json +++ b/packages/schema/schema.json @@ -2033,6 +2033,7 @@ "config_change", "cwd_changed", "file_changed", + "message_display", "notification", "elicitation", "elicitation_result", diff --git a/packages/schema/src/editor-support.ts b/packages/schema/src/editor-support.ts index 74ae267..a38b367 100644 --- a/packages/schema/src/editor-support.ts +++ b/packages/schema/src/editor-support.ts @@ -3,6 +3,7 @@ export const supportedEditorNames = [ 'copilot', 'claude-code', 'windsurf', + 'devin-desktop', 'zed', 'codex', 'gemini', @@ -505,6 +506,7 @@ export const editorSupportProfiles = [ 'ElicitationResult', 'FileChanged', 'InstructionsLoaded', + 'MessageDisplay', 'Notification', 'PermissionDenied', 'PermissionRequest', @@ -969,6 +971,7 @@ export const editorSupportProfiles = [ notes: [ 'OpenCode uses `opencode.json` with a top-level `mcp` object instead of `mcpServers`.', 'Rules are written with section-managed markdown so user-owned AGENTS.md content is preserved.', + 'OpenCode supports plural directory names (e.g., `agents/`, `skills/`) in newer versions.', ], terminology: [ { featureId: 'rules', aixTerm: 'Rules', editorTerm: 'AGENTS.md' }, @@ -1050,6 +1053,111 @@ export const editorSupportProfiles = [ ), }, }, + { + id: 'devin-desktop', + name: 'Devin Desktop', + summary: 'Devin Desktop (formerly Windsurf) with native rules, workflows, skills, and hooks.', + migrationPitch: 'Devin Desktop is the evolution of Windsurf, optimized for agentic workflows.', + notes: [ + 'Devin Desktop is the rebrand of Windsurf.', + 'It supports plural directory names and the `.devinignore` file.', + ], + terminology: [ + { featureId: 'prompts', aixTerm: 'Prompts', editorTerm: 'Workflows' }, + { featureId: 'rules', aixTerm: 'Rules', editorTerm: 'Rules' }, + ], + features: { + rules: feature( + 'rules', + 'native', + 'Rules', + 'Markdown rule files with trigger frontmatter.', + nativeScope('.windsurf/rules/*.md'), + nativeScope('~/.codeium/windsurf/memories/global_rules.md'), + { + supportedValues: [ 'always_on', 'model_decision', 'glob', 'manual' ], + }, + ), + prompts: feature( + 'prompts', + 'native', + 'Workflows', + 'Markdown workflow files with YAML frontmatter.', + nativeScope('.windsurf/workflows/*.md'), + nativeScope('~/.codeium/windsurf/global_workflows/*.md'), + { + supportedValues: [ 'description' ], + }, + ), + agents: feature( + 'agents', + 'unsupported', + 'Agents', + 'Devin Desktop does not document custom agent files that aix can write safely.', + unsupportedScope('aix reports configured agents as unsupported for Devin Desktop.'), + unsupportedScope('aix reports configured agents as unsupported for Devin Desktop.'), + ), + mcp: feature( + 'mcp', + 'native', + 'MCP servers', + 'Global-only JSON MCP configuration.', + unsupportedScope('Devin Desktop does not have a project-scoped MCP config file in aix.'), + nativeScope('~/.codeium/windsurf/mcp_config.json'), + ), + skills: feature( + 'skills', + 'native', + 'Skills', + 'Symlinked native skill directories backed by `.aix/skills/`.', + nativeScope('.windsurf/skills/{name}/'), + nativeScope('~/.windsurf/skills/{name}/'), + { + notes: [ 'Devin Desktop also discovers `.agents/skills/` for compatibility.' ], + }, + ), + hooks: feature( + 'hooks', + 'native', + 'Hooks', + 'JSON hook configuration using snake_case events.', + nativeScope('.windsurf/hooks.json'), + nativeScope('~/.windsurf/hooks.json'), + { + supportedValues: [ + 'post_cascade_response', + 'post_cascade_response_with_transcript', + 'post_mcp_tool_use', + 'post_read_code', + 'post_run_command', + 'post_setup_worktree', + 'post_write_code', + 'pre_mcp_tool_use', + 'pre_read_code', + 'pre_run_command', + 'pre_user_prompt', + 'pre_write_code', + ], + }, + ), + 'agents-md': feature( + 'agents-md', + 'native', + 'AGENTS.md', + 'Compatibility with AGENTS.md repository files.', + nativeScope('AGENTS.md'), + unsupportedScope('Windsurf documentation focuses on workspace files.'), + ), + 'agents-dir': feature( + 'agents-dir', + 'native', + '.agents/skills', + 'Compatibility with the shared Agent Skills folder convention.', + nativeScope('.agents/skills/{name}/'), + nativeScope('~/.agents/skills/{name}/'), + ), + }, + }, ] as const satisfies readonly EditorSupportProfile[]; export const editorSupportProfileMap = Object.freeze( diff --git a/packages/schema/src/hooks.ts b/packages/schema/src/hooks.ts index b59578e..0809aa1 100644 --- a/packages/schema/src/hooks.ts +++ b/packages/schema/src/hooks.ts @@ -63,6 +63,7 @@ export const hookEventSchema = z 'config_change', 'cwd_changed', 'file_changed', + 'message_display', 'notification', 'elicitation', 'elicitation_result', diff --git a/packages/site/src/lib/editor-research.test.ts b/packages/site/src/lib/editor-research.test.ts index cc8f965..d2f5a83 100644 --- a/packages/site/src/lib/editor-research.test.ts +++ b/packages/site/src/lib/editor-research.test.ts @@ -55,7 +55,9 @@ describe('editor research notes', () => { }); it('resolves the default research root from the monorepo', () => { - expect(getEditorResearchRoot()).toMatch(/docs\/editor-research$/u); + const normalizedRoot = getEditorResearchRoot().replace(/\\/g, '/'); + + expect(normalizedRoot).toMatch(/docs\/editor-research$/u); expect(fs.existsSync(getEditorResearchRoot())).toStrictEqual(true); }); diff --git a/packages/site/src/lib/editor-research.ts b/packages/site/src/lib/editor-research.ts index 96e3f46..b533d4e 100644 --- a/packages/site/src/lib/editor-research.ts +++ b/packages/site/src/lib/editor-research.ts @@ -73,7 +73,7 @@ function isSupportedEditorName(value: unknown): value is SupportedEditorName { } function parseResearchFileContent(content: string): ParsedResearchFile { - const frontmatterMatch = /^---\n(?[\s\S]*?)\n---\n?(?[\s\S]*)$/u.exec(content); + const frontmatterMatch = /^---\r?\n(?[\s\S]*?)\r?\n---\r?\n?(?[\s\S]*)$/u.exec(content); if (!frontmatterMatch?.groups) { throw new Error('Missing YAML frontmatter'); diff --git a/packages/site/src/lib/editor-support.ts b/packages/site/src/lib/editor-support.ts index 6c52188..c906f77 100644 --- a/packages/site/src/lib/editor-support.ts +++ b/packages/site/src/lib/editor-support.ts @@ -16,6 +16,7 @@ export const editorIconNames: Record = { copilot: 'simple-icons:githubcopilot', 'claude-code': 'simple-icons:anthropic', windsurf: 'simple-icons:codeium', + 'devin-desktop': 'simple-icons:codeium', zed: 'simple-icons:zedindustries', codex: 'simple-icons:openai', gemini: 'simple-icons:googlegemini', diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..5fca3f8 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file