From 02a8434609f714f5ef0ca336640970ccc6879885 Mon Sep 17 00:00:00 2001 From: Donald Silveia Date: Mon, 9 Feb 2026 19:14:47 -0300 Subject: [PATCH 1/2] Update agent configs --- README.md | 4 +- docs/agents/project-structure.md | 1 + src/adapters/agents.ts | 144 +++++++++++++++++++++++++++ src/adapters/amp.ts | 11 +++ src/adapters/claude.ts | 11 ++- src/adapters/codex.ts | 11 +++ src/adapters/cursor.ts | 7 ++ src/adapters/devin.ts | 164 +++++++++++++++++++++++++++++++ src/adapters/droid.ts | 13 ++- src/adapters/gemini-cli.ts | 11 +++ src/adapters/github-copilot.ts | 11 +++ src/adapters/kimi-cli.ts | 164 +++++++++++++++++++++++++++++++ src/adapters/opencode.ts | 17 +++- src/adapters/registry.ts | 6 ++ src/adapters/shared-skills.ts | 48 +++++++++ src/adapters/vscode.ts | 7 ++ src/agents/index.ts | 6 ++ src/agents/metadata.ts | 33 ++++++- src/agents/shared-skills.ts | 39 ++++++++ src/commands/init.ts | 12 ++- src/commands/new.ts | 16 ++- src/commands/sync.ts | 25 ++++- src/commands/unsync.ts | 13 ++- src/config/types.ts | 3 + 24 files changed, 760 insertions(+), 17 deletions(-) create mode 100644 src/adapters/agents.ts create mode 100644 src/adapters/devin.ts create mode 100644 src/adapters/kimi-cli.ts create mode 100644 src/adapters/shared-skills.ts create mode 100644 src/agents/shared-skills.ts diff --git a/README.md b/README.md index d61526a..789b153 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,8 @@ Global configuration is stored at `~/.syncode/config.json`: ├── packages-arch.txt ├── packages-debian.txt ├── README.md +├── .agents/ # Shared skills (symlinked) +│ └── skills/ └── configs/ ├── amp/ # Symlinked ├── antigravity/ # Copy sync @@ -215,7 +217,7 @@ Global configuration is stored at `~/.syncode/config.json`: ```bash # Edit your AI agent configs normally # Example: ~/.config/opencode/opencode.json -# Example: ~/.claude/skills/my-helper.md +# Example: ~/.agents/skills/my-helper.md # Changes are synced via symlinks automatically # Check what changed diff --git a/docs/agents/project-structure.md b/docs/agents/project-structure.md index 9798de2..a74258b 100644 --- a/docs/agents/project-structure.md +++ b/docs/agents/project-structure.md @@ -7,3 +7,4 @@ - `src/config/`: Configuration management (manager.ts, types.ts). - `src/utils/`: Shared helpers (fs, git, paths, shell, platform). - `configs/`: In the user's repo, stores tracked agent configs. +- `.agents/`: In the user's repo, stores shared skills (symlinked). diff --git a/src/adapters/agents.ts b/src/adapters/agents.ts new file mode 100644 index 0000000..7b048a9 --- /dev/null +++ b/src/adapters/agents.ts @@ -0,0 +1,144 @@ +/** + * Shared Agents adapter + */ + +import { unlinkSync } from "node:fs"; +import { join } from "node:path"; +import { + copyDir, + createSymlink, + ensureDir, + exists, + getSymlinkTarget, + isSymlink, + removeDir, +} from "../utils/fs"; +import { contractHome } from "../utils/paths"; +import type { + AgentAdapter, + ExportResult, + ImportResult, + Platform, +} from "./types"; + +export class AgentsAdapter implements AgentAdapter { + readonly id = "agents"; + readonly name = "Shared Agents"; + readonly version = "1.0.0"; + readonly syncStrategy = { + import: "copy" as const, + export: "symlink" as const, + }; + + getConfigPath(_platform: Platform): string { + return join(process.env.HOME || "", ".agents"); + } + + getRepoPath(repoRoot: string): string { + return join(repoRoot, ".agents"); + } + + getSkillsPath(_platform: Platform): string { + return join(this.getConfigPath(_platform), "skills"); + } + + isInstalled(platform: Platform): boolean { + return exists(this.getConfigPath(platform)); + } + + detect(): boolean { + const platform = + process.platform === "darwin" + ? "macos" + : process.platform === "win32" + ? "windows" + : "linux"; + return this.isInstalled(platform); + } + + isLinked(systemPath: string, repoPath: string): boolean { + if (!exists(systemPath) || !exists(repoPath) || !isSymlink(systemPath)) { + return false; + } + return getSymlinkTarget(systemPath) === repoPath; + } + + async import(systemPath: string, repoPath: string): Promise { + if (!exists(systemPath)) { + return { + success: false, + message: "Shared agents config not found on system", + }; + } + + if (isSymlink(systemPath)) { + return { + success: true, + message: "Already linked to repo - no import needed", + }; + } + + if (exists(repoPath)) { + return { + success: true, + message: "Configs already in repo - no import needed", + }; + } + + ensureDir(repoPath); + copyDir(systemPath, repoPath); + + return { + success: true, + message: "Imported shared agents configs to repo", + }; + } + + async export(repoPath: string, systemPath: string): Promise { + if (!exists(repoPath)) { + return { + success: false, + message: "Shared agents configs not found in repo", + }; + } + + ensureDir(join(repoPath, "skills")); + + if (isSymlink(systemPath)) { + const target = getSymlinkTarget(systemPath); + if (target === repoPath) { + return { + success: true, + message: "Already linked to repo - no export needed", + linkedTo: repoPath, + }; + } + } + + if (exists(systemPath)) { + if (isSymlink(systemPath)) { + unlinkSync(systemPath); + } else { + const backupPath = `${systemPath}.backup`; + if (exists(backupPath)) { + if (isSymlink(backupPath)) { + unlinkSync(backupPath); + } else { + removeDir(backupPath); + } + } + require("node:fs").renameSync(systemPath, backupPath); + } + } + + createSymlink(repoPath, systemPath); + + return { + success: true, + message: `Linked shared agents configs to ${contractHome(systemPath)}`, + linkedTo: repoPath, + }; + } +} + +export const agentsAdapter = new AgentsAdapter(); diff --git a/src/adapters/amp.ts b/src/adapters/amp.ts index 00ba756..8d67d15 100644 --- a/src/adapters/amp.ts +++ b/src/adapters/amp.ts @@ -14,6 +14,11 @@ import { removeDir, } from "../utils/fs"; import { contractHome } from "../utils/paths"; +import { + getSharedSkillsPath, + getSharedSkillsRepoPath, + linkSharedSkillsInRepo, +} from "./shared-skills"; import type { AgentAdapter, ExportResult, @@ -34,6 +39,10 @@ export class AmpAdapter implements AgentAdapter { return join(process.env.HOME || "", ".config/amp"); } + getSkillsPath(_platform: Platform): string { + return getSharedSkillsPath(); + } + getRepoPath(repoRoot: string): string { return join(repoRoot, "configs", "amp"); } @@ -130,6 +139,8 @@ export class AmpAdapter implements AgentAdapter { // Create symlink createSymlink(repoPath, systemPath); + linkSharedSkillsInRepo(repoPath, getSharedSkillsRepoPath(repoPath)); + return { success: true, message: `Linked Amp configs to ${contractHome(systemPath)}`, diff --git a/src/adapters/claude.ts b/src/adapters/claude.ts index 0e32e1e..42a4602 100644 --- a/src/adapters/claude.ts +++ b/src/adapters/claude.ts @@ -12,6 +12,7 @@ import { isSymlink, } from "../utils/fs"; import { contractHome } from "../utils/paths"; +import { getSharedSkillsPath, linkSharedSkillsOnSystem } from "./shared-skills"; import type { AgentAdapter, CanonicalSkill, @@ -30,7 +31,7 @@ export class ClaudeAdapter implements AgentAdapter { }; // Files/folders to sync (exclude cache, history, etc.) - private syncPatterns = ["settings.json", "CLAUDE.md", "commands", "skills"]; + private syncPatterns = ["settings.json", "CLAUDE.md", "commands"]; getConfigPath(platform: Platform): string { if (platform === "windows") { @@ -39,8 +40,8 @@ export class ClaudeAdapter implements AgentAdapter { return join(process.env.HOME || "", ".claude"); } - getSkillsPath(platform: Platform): string { - return join(this.getConfigPath(platform), "skills"); + getSkillsPath(_platform: Platform): string { + return getSharedSkillsPath(); } getRepoPath(repoRoot: string): string { @@ -107,7 +108,7 @@ export class ClaudeAdapter implements AgentAdapter { return { success: true, message: - "No Claude configs found to import (settings.json, CLAUDE.md, commands/, skills/)", + "No Claude configs found to import (settings.json, CLAUDE.md, commands/)", }; } @@ -150,6 +151,8 @@ export class ClaudeAdapter implements AgentAdapter { } } + linkSharedSkillsOnSystem(join(systemPath, "skills")); + return { success: true, message: `Copied Claude configs to ${contractHome(systemPath)}`, diff --git a/src/adapters/codex.ts b/src/adapters/codex.ts index f9a6e7c..e4159d5 100644 --- a/src/adapters/codex.ts +++ b/src/adapters/codex.ts @@ -14,6 +14,11 @@ import { removeDir, } from "../utils/fs"; import { contractHome } from "../utils/paths"; +import { + getSharedSkillsPath, + getSharedSkillsRepoPath, + linkSharedSkillsInRepo, +} from "./shared-skills"; import type { AgentAdapter, ExportResult, @@ -34,6 +39,10 @@ export class CodexAdapter implements AgentAdapter { return join(process.env.HOME || "", ".codex"); } + getSkillsPath(_platform: Platform): string { + return getSharedSkillsPath(); + } + getRepoPath(repoRoot: string): string { return join(repoRoot, "configs", "codex"); } @@ -130,6 +139,8 @@ export class CodexAdapter implements AgentAdapter { // Create symlink createSymlink(repoPath, systemPath); + linkSharedSkillsInRepo(repoPath, getSharedSkillsRepoPath(repoPath)); + return { success: true, message: `Linked Codex configs to ${contractHome(systemPath)}`, diff --git a/src/adapters/cursor.ts b/src/adapters/cursor.ts index 10b7db9..16e7187 100644 --- a/src/adapters/cursor.ts +++ b/src/adapters/cursor.ts @@ -21,6 +21,7 @@ import { removeDir, } from "../utils/fs"; import { contractHome } from "../utils/paths"; +import { getSharedSkillsPath, linkSharedSkillsOnSystem } from "./shared-skills"; import type { AgentAdapter, CanonicalSkill, @@ -63,6 +64,10 @@ export class CursorAdapter implements AgentAdapter { } } + getSkillsPath(_platform: Platform): string { + return getSharedSkillsPath(); + } + getRepoPath(repoRoot: string): string { return join(repoRoot, "configs", "cursor"); } @@ -137,6 +142,8 @@ export class CursorAdapter implements AgentAdapter { }; } + linkSharedSkillsOnSystem(join(systemPath, "skills")); + return { success: true, message: "Imported Cursor configs to repo", diff --git a/src/adapters/devin.ts b/src/adapters/devin.ts new file mode 100644 index 0000000..3db7898 --- /dev/null +++ b/src/adapters/devin.ts @@ -0,0 +1,164 @@ +/** + * Devin adapter + */ + +import { unlinkSync } from "node:fs"; +import { join } from "node:path"; +import { + copyDir, + createSymlink, + ensureDir, + exists, + getSymlinkTarget, + isSymlink, + removeDir, +} from "../utils/fs"; +import { contractHome } from "../utils/paths"; +import { + getSharedSkillsPath, + getSharedSkillsRepoPath, + linkSharedSkillsInRepo, +} from "./shared-skills"; +import type { + AgentAdapter, + ExportResult, + ImportResult, + Platform, +} from "./types"; + +export class DevinAdapter implements AgentAdapter { + readonly id = "devin"; + readonly name = "Devin"; + readonly version = "1.0.0"; + readonly syncStrategy = { + import: "copy" as const, + export: "symlink" as const, + }; + + private getConfigPaths(platform: Platform): string[] { + if (platform === "windows") { + return [join(process.env.APPDATA || "", "devin")]; + } + return [ + join(process.env.HOME || "", ".devin"), + join(process.env.HOME || "", ".config", "devin"), + ]; + } + + getConfigPath(platform: Platform): string { + for (const path of this.getConfigPaths(platform)) { + if (exists(path)) { + return path; + } + } + return this.getConfigPaths(platform)[0]!; + } + + getSkillsPath(_platform: Platform): string { + return getSharedSkillsPath(); + } + + getRepoPath(repoRoot: string): string { + return join(repoRoot, "configs", "devin"); + } + + isInstalled(platform: Platform): boolean { + return this.getConfigPaths(platform).some((path) => exists(path)); + } + + detect(): boolean { + const platform = + process.platform === "darwin" + ? "macos" + : process.platform === "win32" + ? "windows" + : "linux"; + return this.isInstalled(platform); + } + + isLinked(systemPath: string, repoPath: string): boolean { + if (!exists(systemPath) || !exists(repoPath) || !isSymlink(systemPath)) { + return false; + } + return getSymlinkTarget(systemPath) === repoPath; + } + + async import(systemPath: string, repoPath: string): Promise { + if (!exists(systemPath)) { + return { + success: false, + message: "Devin config not found on system", + }; + } + + if (isSymlink(systemPath)) { + return { + success: true, + message: "Already linked to repo - no import needed", + }; + } + + if (exists(repoPath)) { + return { + success: true, + message: "Configs already in repo - no import needed", + }; + } + + ensureDir(repoPath); + copyDir(systemPath, repoPath); + + return { + success: true, + message: "Imported Devin configs to repo", + }; + } + + async export(repoPath: string, systemPath: string): Promise { + if (!exists(repoPath)) { + return { + success: false, + message: "Devin configs not found in repo", + }; + } + + if (isSymlink(systemPath)) { + const target = getSymlinkTarget(systemPath); + if (target === repoPath) { + return { + success: true, + message: "Already linked to repo - no export needed", + linkedTo: repoPath, + }; + } + } + + if (exists(systemPath)) { + if (isSymlink(systemPath)) { + unlinkSync(systemPath); + } else { + const backupPath = `${systemPath}.backup`; + if (exists(backupPath)) { + if (isSymlink(backupPath)) { + unlinkSync(backupPath); + } else { + removeDir(backupPath); + } + } + require("node:fs").renameSync(systemPath, backupPath); + } + } + + createSymlink(repoPath, systemPath); + + linkSharedSkillsInRepo(repoPath, getSharedSkillsRepoPath(repoPath)); + + return { + success: true, + message: `Linked Devin configs to ${contractHome(systemPath)}`, + linkedTo: repoPath, + }; + } +} + +export const devinAdapter = new DevinAdapter(); diff --git a/src/adapters/droid.ts b/src/adapters/droid.ts index 2449ce0..83e5463 100644 --- a/src/adapters/droid.ts +++ b/src/adapters/droid.ts @@ -14,6 +14,11 @@ import { removeDir, } from "../utils/fs"; import { contractHome } from "../utils/paths"; +import { + getSharedSkillsPath, + getSharedSkillsRepoPath, + linkSharedSkillsInRepo, +} from "./shared-skills"; import type { AgentAdapter, ExportResult, @@ -34,12 +39,16 @@ export class DroidAdapter implements AgentAdapter { return join(process.env.HOME || "", ".factory"); } + getSkillsPath(_platform: Platform): string { + return getSharedSkillsPath(); + } + getRepoPath(repoRoot: string): string { return join(repoRoot, "configs", "droid"); } isInstalled(platform: Platform): boolean { - return exists(join(this.getConfigPath(platform), "skills")); + return exists(this.getConfigPath(platform)); } detect(): boolean { @@ -130,6 +139,8 @@ export class DroidAdapter implements AgentAdapter { // Create symlink createSymlink(repoPath, systemPath); + linkSharedSkillsInRepo(repoPath, getSharedSkillsRepoPath(repoPath)); + return { success: true, message: `Linked Droid configs to ${contractHome(systemPath)}`, diff --git a/src/adapters/gemini-cli.ts b/src/adapters/gemini-cli.ts index 7aa5f15..ef85aad 100644 --- a/src/adapters/gemini-cli.ts +++ b/src/adapters/gemini-cli.ts @@ -14,6 +14,11 @@ import { removeDir, } from "../utils/fs"; import { contractHome } from "../utils/paths"; +import { + getSharedSkillsPath, + getSharedSkillsRepoPath, + linkSharedSkillsInRepo, +} from "./shared-skills"; import type { AgentAdapter, ExportResult, @@ -34,6 +39,10 @@ export class GeminiCliAdapter implements AgentAdapter { return join(process.env.HOME || "", ".gemini"); } + getSkillsPath(_platform: Platform): string { + return getSharedSkillsPath(); + } + getRepoPath(repoRoot: string): string { return join(repoRoot, "configs", "gemini-cli"); } @@ -130,6 +139,8 @@ export class GeminiCliAdapter implements AgentAdapter { // Create symlink createSymlink(repoPath, systemPath); + linkSharedSkillsInRepo(repoPath, getSharedSkillsRepoPath(repoPath)); + return { success: true, message: `Linked Gemini CLI configs to ${contractHome(systemPath)}`, diff --git a/src/adapters/github-copilot.ts b/src/adapters/github-copilot.ts index e2d3ccd..2553780 100644 --- a/src/adapters/github-copilot.ts +++ b/src/adapters/github-copilot.ts @@ -14,6 +14,11 @@ import { removeDir, } from "../utils/fs"; import { contractHome } from "../utils/paths"; +import { + getSharedSkillsPath, + getSharedSkillsRepoPath, + linkSharedSkillsInRepo, +} from "./shared-skills"; import type { AgentAdapter, ExportResult, @@ -34,6 +39,10 @@ export class GithubCopilotAdapter implements AgentAdapter { return join(process.env.HOME || "", ".copilot"); } + getSkillsPath(_platform: Platform): string { + return getSharedSkillsPath(); + } + getRepoPath(repoRoot: string): string { return join(repoRoot, "configs", "github-copilot"); } @@ -132,6 +141,8 @@ export class GithubCopilotAdapter implements AgentAdapter { // Create symlink createSymlink(repoPath, systemPath); + linkSharedSkillsInRepo(repoPath, getSharedSkillsRepoPath(repoPath)); + return { success: true, message: `Linked GitHub Copilot configs to ${contractHome(systemPath)}`, diff --git a/src/adapters/kimi-cli.ts b/src/adapters/kimi-cli.ts new file mode 100644 index 0000000..1d69b36 --- /dev/null +++ b/src/adapters/kimi-cli.ts @@ -0,0 +1,164 @@ +/** + * Kimi CLI adapter + */ + +import { unlinkSync } from "node:fs"; +import { join } from "node:path"; +import { + copyDir, + createSymlink, + ensureDir, + exists, + getSymlinkTarget, + isSymlink, + removeDir, +} from "../utils/fs"; +import { contractHome } from "../utils/paths"; +import { + getSharedSkillsPath, + getSharedSkillsRepoPath, + linkSharedSkillsInRepo, +} from "./shared-skills"; +import type { + AgentAdapter, + ExportResult, + ImportResult, + Platform, +} from "./types"; + +export class KimiCliAdapter implements AgentAdapter { + readonly id = "kimi-cli"; + readonly name = "Kimi CLI"; + readonly version = "1.0.0"; + readonly syncStrategy = { + import: "copy" as const, + export: "symlink" as const, + }; + + private getConfigPaths(platform: Platform): string[] { + if (platform === "windows") { + return [join(process.env.APPDATA || "", "kimi")]; + } + return [ + join(process.env.HOME || "", ".kimi"), + join(process.env.HOME || "", ".config", "kimi"), + ]; + } + + getConfigPath(platform: Platform): string { + for (const path of this.getConfigPaths(platform)) { + if (exists(path)) { + return path; + } + } + return this.getConfigPaths(platform)[0]!; + } + + getSkillsPath(_platform: Platform): string { + return getSharedSkillsPath(); + } + + getRepoPath(repoRoot: string): string { + return join(repoRoot, "configs", "kimi-cli"); + } + + isInstalled(platform: Platform): boolean { + return this.getConfigPaths(platform).some((path) => exists(path)); + } + + detect(): boolean { + const platform = + process.platform === "darwin" + ? "macos" + : process.platform === "win32" + ? "windows" + : "linux"; + return this.isInstalled(platform); + } + + isLinked(systemPath: string, repoPath: string): boolean { + if (!exists(systemPath) || !exists(repoPath) || !isSymlink(systemPath)) { + return false; + } + return getSymlinkTarget(systemPath) === repoPath; + } + + async import(systemPath: string, repoPath: string): Promise { + if (!exists(systemPath)) { + return { + success: false, + message: "Kimi CLI config not found on system", + }; + } + + if (isSymlink(systemPath)) { + return { + success: true, + message: "Already linked to repo - no import needed", + }; + } + + if (exists(repoPath)) { + return { + success: true, + message: "Configs already in repo - no import needed", + }; + } + + ensureDir(repoPath); + copyDir(systemPath, repoPath); + + return { + success: true, + message: "Imported Kimi CLI configs to repo", + }; + } + + async export(repoPath: string, systemPath: string): Promise { + if (!exists(repoPath)) { + return { + success: false, + message: "Kimi CLI configs not found in repo", + }; + } + + if (isSymlink(systemPath)) { + const target = getSymlinkTarget(systemPath); + if (target === repoPath) { + return { + success: true, + message: "Already linked to repo - no export needed", + linkedTo: repoPath, + }; + } + } + + if (exists(systemPath)) { + if (isSymlink(systemPath)) { + unlinkSync(systemPath); + } else { + const backupPath = `${systemPath}.backup`; + if (exists(backupPath)) { + if (isSymlink(backupPath)) { + unlinkSync(backupPath); + } else { + removeDir(backupPath); + } + } + require("node:fs").renameSync(systemPath, backupPath); + } + } + + createSymlink(repoPath, systemPath); + + linkSharedSkillsInRepo(repoPath, getSharedSkillsRepoPath(repoPath)); + + return { + success: true, + message: `Linked Kimi CLI configs to ${contractHome(systemPath)}`, + linkedTo: repoPath, + }; + } +} + +export const kimiCliAdapter = new KimiCliAdapter(); diff --git a/src/adapters/opencode.ts b/src/adapters/opencode.ts index cce2b6f..d1e9458 100644 --- a/src/adapters/opencode.ts +++ b/src/adapters/opencode.ts @@ -21,6 +21,11 @@ import { removeDir, } from "../utils/fs"; import { contractHome } from "../utils/paths"; +import { + getSharedSkillsPath, + getSharedSkillsRepoPath, + linkSharedSkillsInRepo, +} from "./shared-skills"; import type { AgentAdapter, CanonicalSkill, @@ -39,7 +44,7 @@ export class OpenCodeAdapter implements AgentAdapter { }; // Files/folders to sync (exclude node_modules, cache, etc.) - private syncPatterns = ["opencode.json", "command", "agent", "skill"]; + private syncPatterns = ["opencode.json", "command", "agent"]; getConfigPath(platform: Platform): string { // OpenCode uses XDG on all platforms @@ -49,8 +54,8 @@ export class OpenCodeAdapter implements AgentAdapter { return join(process.env.HOME || "", ".config", "opencode"); } - getSkillsPath(platform: Platform): string { - return join(this.getConfigPath(platform), "skill"); + getSkillsPath(_platform: Platform): string { + return getSharedSkillsPath(); } getRepoPath(repoRoot: string): string { @@ -166,6 +171,12 @@ export class OpenCodeAdapter implements AgentAdapter { ensureDir(dirname(systemPath)); symlinkSync(repoPath, systemPath); + linkSharedSkillsInRepo( + repoPath, + getSharedSkillsRepoPath(repoPath), + "skill", + ); + return { success: true, message: `Linked ${contractHome(systemPath)} → repo`, diff --git a/src/adapters/registry.ts b/src/adapters/registry.ts index 6b82625..29841c7 100644 --- a/src/adapters/registry.ts +++ b/src/adapters/registry.ts @@ -1,15 +1,18 @@ +import { agentsAdapter } from "./agents"; import { ampAdapter } from "./amp"; import { antigravityAdapter } from "./antigravity"; import { claudeAdapter } from "./claude"; import { clawdbotAdapter } from "./clawdbot"; import { codexAdapter } from "./codex"; import { cursorAdapter } from "./cursor"; +import { devinAdapter } from "./devin"; import { dotfilesAdapter } from "./dotfiles"; import { droidAdapter } from "./droid"; import { geminiCliAdapter } from "./gemini-cli"; import { githubCopilotAdapter } from "./github-copilot"; import { gooseAdapter } from "./goose"; import { kiloAdapter } from "./kilo"; +import { kimiCliAdapter } from "./kimi-cli"; import { kiroCliAdapter } from "./kiro-cli"; import { opencodeAdapter } from "./opencode"; import { rooAdapter } from "./roo"; @@ -77,17 +80,20 @@ export class AdapterRegistry { export const adapterRegistry = new AdapterRegistry(); export function registerBuiltinAdapters(): void { adapterRegistry.register(ampAdapter); + adapterRegistry.register(agentsAdapter); adapterRegistry.register(antigravityAdapter); adapterRegistry.register(claudeAdapter); adapterRegistry.register(clawdbotAdapter); adapterRegistry.register(codexAdapter); adapterRegistry.register(cursorAdapter); + adapterRegistry.register(devinAdapter); adapterRegistry.register(dotfilesAdapter); adapterRegistry.register(droidAdapter); adapterRegistry.register(geminiCliAdapter); adapterRegistry.register(githubCopilotAdapter); adapterRegistry.register(gooseAdapter); adapterRegistry.register(kiloAdapter); + adapterRegistry.register(kimiCliAdapter); adapterRegistry.register(kiroCliAdapter); adapterRegistry.register(opencodeAdapter); adapterRegistry.register(rooAdapter); diff --git a/src/adapters/shared-skills.ts b/src/adapters/shared-skills.ts new file mode 100644 index 0000000..5c87c16 --- /dev/null +++ b/src/adapters/shared-skills.ts @@ -0,0 +1,48 @@ +import { dirname, join, relative, resolve } from "node:path"; +import { + createSymlinkWithBackup, + ensureDir, + getSymlinkTarget, + isSymlink, +} from "../utils/fs"; + +export const SHARED_SKILLS_DIRNAME = "skills"; + +export function getSharedSkillsPath(): string { + return join(process.env.HOME || "", ".agents", SHARED_SKILLS_DIRNAME); +} + +export function getSharedSkillsRepoPath(repoPath: string): string { + return resolve(repoPath, "..", "..", ".agents", SHARED_SKILLS_DIRNAME); +} + +export function linkSharedSkillsOnSystem( + agentSkillsPath: string, + sharedSkillsPath: string = getSharedSkillsPath(), +): void { + ensureDir(sharedSkillsPath); + if (isSymlink(agentSkillsPath)) { + const target = getSymlinkTarget(agentSkillsPath); + if (target === sharedSkillsPath) { + return; + } + } + createSymlinkWithBackup(sharedSkillsPath, agentSkillsPath); +} + +export function linkSharedSkillsInRepo( + agentRepoPath: string, + sharedSkillsRepoPath: string, + agentSkillsDirName: string = SHARED_SKILLS_DIRNAME, +): void { + ensureDir(sharedSkillsRepoPath); + const linkPath = join(agentRepoPath, agentSkillsDirName); + const relativeTarget = relative(dirname(linkPath), sharedSkillsRepoPath); + if (isSymlink(linkPath)) { + const target = getSymlinkTarget(linkPath); + if (target === relativeTarget) { + return; + } + } + createSymlinkWithBackup(relativeTarget, linkPath); +} diff --git a/src/adapters/vscode.ts b/src/adapters/vscode.ts index 216efb6..028ee05 100644 --- a/src/adapters/vscode.ts +++ b/src/adapters/vscode.ts @@ -21,6 +21,7 @@ import { removeDir, } from "../utils/fs"; import { contractHome } from "../utils/paths"; +import { getSharedSkillsPath, linkSharedSkillsOnSystem } from "./shared-skills"; import type { AgentAdapter, CanonicalSkill, @@ -64,6 +65,10 @@ export class VSCodeAdapter implements AgentAdapter { } } + getSkillsPath(_platform: Platform): string { + return getSharedSkillsPath(); + } + getRepoPath(repoRoot: string): string { return join(repoRoot, "configs", "vscode"); } @@ -138,6 +143,8 @@ export class VSCodeAdapter implements AgentAdapter { }; } + linkSharedSkillsOnSystem(join(systemPath, "skills")); + return { success: true, message: "Imported VSCode configs to repo", diff --git a/src/agents/index.ts b/src/agents/index.ts index fd33aef..3796e1d 100644 --- a/src/agents/index.ts +++ b/src/agents/index.ts @@ -8,3 +8,9 @@ export { getAllAgentIds, isAgentInstalled, } from "./metadata"; +export { + ensureSharedSkillsAgent, + SHARED_SKILLS_AGENT_ID, + sortSharedSkillsFirst, + usesSharedSkills, +} from "./shared-skills"; diff --git a/src/agents/metadata.ts b/src/agents/metadata.ts index 7f6d6a1..71eb98c 100644 --- a/src/agents/metadata.ts +++ b/src/agents/metadata.ts @@ -15,6 +15,14 @@ export interface AgentMetadata { const home = homedir(); export const agentMetadata: Record = { + agents: { + id: "agents", + displayName: "Shared Agents", + configDir: join(home, ".agents"), + detectInstalled: () => existsSync(join(home, ".agents")), + hasAdapter: true, + }, + amp: { id: "amp", displayName: "Amp", @@ -94,11 +102,23 @@ export const agentMetadata: Record = { hasAdapter: true, }, + devin: { + id: "devin", + displayName: "Devin", + configDir: join(home, ".devin"), + detectInstalled: () => + existsSync(join(home, ".devin")) || + existsSync(join(home, ".config/devin")), + hasAdapter: true, + }, + droid: { id: "droid", displayName: "Droid", configDir: join(home, ".factory"), - detectInstalled: () => existsSync(join(home, ".factory/skills")), + detectInstalled: () => + existsSync(join(home, ".factory")) || + existsSync(join(home, ".agents/skills")), hasAdapter: true, }, @@ -136,6 +156,15 @@ export const agentMetadata: Record = { hasAdapter: true, }, + "kimi-cli": { + id: "kimi-cli", + displayName: "Kimi CLI", + configDir: join(home, ".kimi"), + detectInstalled: () => + existsSync(join(home, ".kimi")) || existsSync(join(home, ".config/kimi")), + hasAdapter: true, + }, + "kiro-cli": { id: "kiro-cli", displayName: "Kiro CLI", @@ -150,7 +179,7 @@ export const agentMetadata: Record = { configDir: join(home, ".config/opencode"), detectInstalled: () => existsSync(join(home, ".config/opencode")) || - existsSync(join(home, ".claude/skills")), + existsSync(join(home, ".agents/skills")), hasAdapter: true, }, diff --git a/src/agents/shared-skills.ts b/src/agents/shared-skills.ts new file mode 100644 index 0000000..21eb8e4 --- /dev/null +++ b/src/agents/shared-skills.ts @@ -0,0 +1,39 @@ +export const SHARED_SKILLS_AGENT_ID = "agents"; + +const sharedSkillsAgents = new Set([ + "amp", + "claude", + "codex", + "cursor", + "devin", + "droid", + "gemini-cli", + "github-copilot", + "kimi-cli", + "opencode", + "vscode", +]); + +export function usesSharedSkills(agentId: string): boolean { + return sharedSkillsAgents.has(agentId); +} + +export function ensureSharedSkillsAgent(agentIds: string[]): string[] { + if (!agentIds.some(usesSharedSkills)) { + return agentIds; + } + if (agentIds.includes(SHARED_SKILLS_AGENT_ID)) { + return agentIds; + } + return [...agentIds, SHARED_SKILLS_AGENT_ID]; +} + +export function sortSharedSkillsFirst(agentIds: string[]): string[] { + if (!agentIds.includes(SHARED_SKILLS_AGENT_ID)) { + return agentIds; + } + return [ + SHARED_SKILLS_AGENT_ID, + ...agentIds.filter((id) => id !== SHARED_SKILLS_AGENT_ID), + ]; +} diff --git a/src/commands/init.ts b/src/commands/init.ts index 6346d50..36d72f6 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -6,8 +6,10 @@ import { adapterRegistry } from "../adapters/registry"; import type { Platform } from "../adapters/types"; import { detectInstalledAgents, + ensureSharedSkillsAgent, getAgentMetadata, getAgentsWithAdapters, + usesSharedSkills, } from "../agents"; import { configExists, initConfig } from "../config/manager"; import { SUPPORTED_AGENTS } from "../config/types"; @@ -147,7 +149,15 @@ export async function initCommand() { return; } - const selectedAgents = agentsInput as string[]; + let selectedAgents = agentsInput as string[]; + const expandedAgents = ensureSharedSkillsAgent(selectedAgents); + if ( + expandedAgents.length > selectedAgents.length && + selectedAgents.some(usesSharedSkills) + ) { + p.log.info("Including Shared Agents (.agents) for shared skills"); + } + selectedAgents = expandedAgents; if (selectedAgents.length === 0) { p.log.warn( diff --git a/src/commands/new.ts b/src/commands/new.ts index ad91cdf..51f4bd3 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -6,8 +6,10 @@ import { adapterRegistry } from "../adapters/registry"; import type { Platform } from "../adapters/types"; import { detectInstalledAgents, + ensureSharedSkillsAgent, getAgentMetadata, getAgentsWithAdapters, + usesSharedSkills, } from "../agents"; import { configExists, initConfig } from "../config/manager"; import { SUPPORTED_AGENTS } from "../config/types"; @@ -154,7 +156,15 @@ export async function newCommand() { return; } - const selectedAgents = agentsInput as string[]; + let selectedAgents = agentsInput as string[]; + const expandedAgents = ensureSharedSkillsAgent(selectedAgents); + if ( + expandedAgents.length > selectedAgents.length && + selectedAgents.some(usesSharedSkills) + ) { + p.log.info("Including Shared Agents (.agents) for shared skills"); + } + selectedAgents = expandedAgents; if (selectedAgents.length === 0) { p.log.warn( @@ -193,6 +203,10 @@ export async function newCommand() { const configsDir = join(repoPath, "configs"); mkdirSync(configsDir, { recursive: true }); + if (selectedAgents.includes("agents")) { + mkdirSync(join(repoPath, ".agents", "skills"), { recursive: true }); + } + const templateFiles = [ { label: "Brewfile", diff --git a/src/commands/sync.ts b/src/commands/sync.ts index a4f4b4e..af9cd18 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -5,6 +5,11 @@ import * as p from "@clack/prompts"; import { adapterRegistry } from "../adapters/registry"; import type { Platform } from "../adapters/types"; +import { + ensureSharedSkillsAgent, + sortSharedSkillsFirst, + usesSharedSkills, +} from "../agents"; import { getConfig } from "../config/manager"; import { checkAndMigrateConfig } from "../config/migrations"; import type { GlobalConfig } from "../config/types"; @@ -99,14 +104,28 @@ export async function syncCommand() { } } - if ((selectedAgents as string[]).length === 0) { + let resolvedAgents = selectedAgents as string[]; + if (resolvedAgents.length === 0) { p.cancel("No agents selected"); return; } + const expandedAgents = ensureSharedSkillsAgent(resolvedAgents); + if ( + expandedAgents.length > resolvedAgents.length && + resolvedAgents.some(usesSharedSkills) + ) { + p.log.info("Including Shared Agents (.agents) for shared skills"); + } + + resolvedAgents = + direction === "export" + ? sortSharedSkillsFirst(expandedAgents) + : expandedAgents; + const s = p.spinner(); s.start( - `${direction === "import" ? "Importing" : "Exporting"} ${(selectedAgents as string[]).length} agent(s)`, + `${direction === "import" ? "Importing" : "Exporting"} ${resolvedAgents.length} agent(s)`, ); const repoPath = config.repoPath.startsWith("~") @@ -117,7 +136,7 @@ export async function syncCommand() { let failCount = 0; const errors: Array<{ agent: string; error: string }> = []; - for (const agentId of selectedAgents as string[]) { + for (const agentId of resolvedAgents) { const adapter = adapterRegistry.get(agentId); if (!adapter) { const errorMsg = "Adapter not found"; diff --git a/src/commands/unsync.ts b/src/commands/unsync.ts index d900a23..e11b8bf 100644 --- a/src/commands/unsync.ts +++ b/src/commands/unsync.ts @@ -3,6 +3,7 @@ import { dirname, join } from "node:path"; import * as p from "@clack/prompts"; import { adapterRegistry } from "../adapters/registry"; import type { Platform } from "../adapters/types"; +import { ensureSharedSkillsAgent, usesSharedSkills } from "../agents"; import { getConfig } from "../config/manager"; import type { GlobalConfig } from "../config/types"; import { @@ -82,6 +83,16 @@ export async function unsyncCommand() { return; } + let agentsToUnsync = config.agents; + const expandedAgents = ensureSharedSkillsAgent(agentsToUnsync); + if ( + expandedAgents.length > agentsToUnsync.length && + agentsToUnsync.some(usesSharedSkills) + ) { + p.log.info("Including Shared Agents (.agents) for shared skills"); + } + agentsToUnsync = expandedAgents; + const platform: Platform = process.platform === "darwin" ? "macos" @@ -97,7 +108,7 @@ export async function unsyncCommand() { const s = p.spinner(); s.start("Removing config symlinks"); - for (const agentId of config.agents) { + for (const agentId of agentsToUnsync) { const adapter = adapterRegistry.get(agentId); if (!adapter) { s.message(`Warning: Adapter not found for ${agentId}`); diff --git a/src/config/types.ts b/src/config/types.ts index fdff9f0..5a9f2a6 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -57,18 +57,21 @@ export const DEFAULT_CONFIG: GlobalConfig = { }; export const SUPPORTED_AGENTS = [ + "agents", "amp", "antigravity", "claude", "clawdbot", "codex", "cursor", + "devin", "dotfiles", "droid", "gemini-cli", "github-copilot", "goose", "kilo", + "kimi-cli", "kiro-cli", "opencode", "roo", From e9adfa7e7c24937c11c1b9307da104c4766a91ea Mon Sep 17 00:00:00 2001 From: Donald Silveia Date: Mon, 9 Feb 2026 19:25:52 -0300 Subject: [PATCH 2/2] Improve new command --- src/commands/new.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/commands/new.ts b/src/commands/new.ts index 51f4bd3..6aaaa98 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -11,7 +11,7 @@ import { getAgentsWithAdapters, usesSharedSkills, } from "../agents"; -import { configExists, initConfig } from "../config/manager"; +import { configExists, getConfig, initConfig } from "../config/manager"; import { SUPPORTED_AGENTS } from "../config/types"; import { BREWFILE_TEMPLATE, @@ -24,13 +24,24 @@ export async function newCommand() { p.intro("Initialize Agent Config Repository"); if (configExists()) { - const overwrite = await p.confirm({ - message: - "Configuration already exists at ~/.syncode/config.json. Overwrite?", - initialValue: false, + const existingConfig = getConfig(); + const overwrite = await p.select({ + message: `Configuration already exists at ~/.syncode/config.json (current repo: ${contractHome(expandHome(existingConfig.repoPath))}).`, + options: [ + { + value: "replace", + label: "Replace and continue", + hint: "You can choose a new repo path next", + }, + { + value: "cancel", + label: "Cancel", + hint: "Keep current configuration", + }, + ], }); - if (p.isCancel(overwrite) || !overwrite) { + if (p.isCancel(overwrite) || overwrite !== "replace") { p.cancel("Initialization cancelled."); return; }