Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions docs/plans/agent-mappings-and-link-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Agent Mapping Gap Analysis + Zero-Config Link Command

## Part 1: Missing Agent Mappings

### Current Support Summary

**Supported Agents:** claude, cursor, codex, roocode, opencode, generic

| Feature | Claude | Cursor | Codex | RooCode | OpenCode |
|---------|--------|--------|-------|---------|----------|
| Rules | ✅ `CLAUDE.md` | ✅ `.cursor/rules/` | ✅ `AGENTS.md` | ✅ `.roo/rules/` | ✅ `AGENTS.md` |
| Commands | ✅ `.claude/commands/` | ✅ `.cursor/commands/` | ❌ | ❌ | ✅ `.opencode/command/` |
| Skills | ✅ `.claude/skills/` | ✅ `.cursor/skills/` | ❌ | ❌ | ✅ (Claude path) |
| Agents | ✅ `.claude/agents/` | ✅ `.cursor/agents/` | ❌ | ❌ | ✅ `.opencode/agent/` |
| Hooks | ✅ settings.json | ✅ hooks.json | ❌ | ❌ | ❌ |

---

### Feature 1: Add Factory Agent (New)

Based on [Factory AI docs](https://docs.factory.ai/cli/configuration/settings):

**Project-relative paths:**
| Feature | Path |
|---------|------|
| Rules | `.factory/AGENTS.md` |
| Skills | `.factory/skills/<name>/SKILL.md` |
| Droids | `.factory/droids/` |
| Hooks | `.factory/settings.json` (hooks key) |

**Files to modify:**

1. `src/types/index.ts:3` - Add `'factory'` to AgentName
2. `src/types/index.ts:157-158` - Add to `isValidAgentName`
3. `src/agents/index.ts:9-25` - Add to AGENT_DIRECTORY_MAPPINGS:
```typescript
skills: { factory: '.factory/skills' },
agents: { factory: '.factory/droids' },
```
4. `src/agents/index.ts:38-71` - Add to AGENT_MAPPINGS:
```typescript
factory: {
path: 'AGENTS.md',
format: 'markdown',
directory: '.factory',
mcpPath: '.factory/settings.json'
}
```
5. `src/agents/hooks-distributor.ts` - Add Factory hook distribution (same format as Claude)
6. `src/core/config-loader.ts:152` - Add `'factory'` to validAgentNames

---

### Feature 2: Expand Codex Support

Based on [Codex Custom Prompts docs](https://developers.openai.com/codex/custom-prompts/) and [Skills docs](https://developers.openai.com/codex/skills/):

**Project-relative paths:**
| Feature | Path | Notes |
|---------|------|-------|
| Rules | `.codex/AGENTS.md` | |
| Commands | `.codex/prompts/` | Equivalent to Claude's commands |
| Skills | `.codex/skills/` | Uses SKILL.md format |

**Files to modify:**

1. `src/agents/index.ts:9-25` - Add Codex to AGENT_DIRECTORY_MAPPINGS:
```typescript
commands: { codex: '.codex/prompts' }, // Note: "prompts" not "commands"
skills: { codex: '.codex/skills' },
```
2. `src/agents/index.ts:50-54` - Update Codex in AGENT_MAPPINGS:
```typescript
codex: {
path: 'AGENTS.md',
format: 'markdown',
directory: '.codex', // Add this
mcpPath: '.codex/config.toml' // Update path
}
```

---

## Part 2: Zero-Config Link Command

### Usage
```bash
bunx glooit link # Symlink .agents/ to all supported agents
bunx glooit link .glooit # Symlink from .glooit/ directory
bunx glooit link -t claude,cursor # Symlink to specific agents only
bunx glooit link .glooit -t claude # Combine source dir and targets
```

### How it works

1. Accept optional positional argument for source directory (defaults to `.agents/`, falls back to `.glooit/`)
2. Scan for known patterns:
- `CLAUDE.md` → sync to Claude
- `AGENTS.md` → sync to Codex, OpenCode, Factory
- `commands/` → sync commands directory
- `skills/` → sync skills directory
- `agents/` → sync agents directory
3. Build virtual config with `mode: 'symlink'` and run distributor
4. No `glooit.config.ts` required

### Implementation

Add to `src/cli/index.ts`:

```typescript
program
.command('link')
.description('Zero-config symlink: auto-sync .agents/ to all supported agents')
.argument('[source]', 'source directory (default: .agents, fallback: .glooit)')
.option('-t, --targets <agents>', 'comma-separated list of agents (default: all)')
.action(async (source, options) => {
await linkCommand(source, options.targets);
});
```

New function `linkCommand(source?: string, targets?: string)` (~80 lines):
1. Resolve source dir: use provided arg, or default to `.agents/`, or fall back to `.glooit/`
2. Scan for files/directories
3. Build config object with discovered rules and `mode: 'symlink'`
4. Create `AIRulesCore` and call `sync()`

Note: `--copy` option is intentionally omitted - use `glooit sync` for copy mode (requires config file).

The existing `unlink` command already works without config - it reads from the manifest to find symlinks to replace.

---

## Files to Modify (Summary)

| File | Changes |
|------|---------|
| `src/types/index.ts` | Add `factory` to AgentName, update validation |
| `src/agents/index.ts` | Add Factory config, add Codex/Factory directory mappings |
| `src/agents/hooks-distributor.ts` | Add Factory hook support |
| `src/core/config-loader.ts` | Add `factory` to validAgentNames |
| `src/cli/index.ts` | Add `link` command |

---

## Verification

1. Run existing tests: `bun test`
2. Test Factory agent:
```bash
mkdir -p .agents/skills/test-skill
echo "---\nname: test\n---\nTest" > .agents/skills/test-skill/SKILL.md
bunx glooit link
# Verify .factory/skills/test-skill/SKILL.md exists as symlink
```
3. Test Codex support:
```bash
# Verify .codex/skills/test-skill/SKILL.md exists as symlink
# Verify .codex/prompts/ has commands symlinked
```
4. Test zero-config link:
```bash
rm glooit.config.ts # Remove config
bunx glooit link # Should still work
```
78 changes: 77 additions & 1 deletion src/agents/hooks-distributor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,18 @@ export class AgentHooksDistributor {
// Group hooks by target agent
const claudeHooks: AgentHook[] = [];
const cursorHooks: AgentHook[] = [];
const factoryHooks: AgentHook[] = [];

for (const hook of this.config.hooks) {
for (const target of hook.targets) {
if (target === 'claude') {
claudeHooks.push(hook);
} else if (target === 'cursor') {
cursorHooks.push(hook);
} else if (target === 'factory') {
factoryHooks.push(hook);
}
// codex and roocode don't support hooks
// codex, roocode, opencode don't support hooks
}
}

Expand All @@ -85,6 +88,10 @@ export class AgentHooksDistributor {
if (cursorHooks.length > 0) {
await this.distributeCursorHooks(cursorHooks);
}

if (factoryHooks.length > 0) {
await this.distributeFactoryHooks(factoryHooks);
}
}

private async distributeClaudeHooks(hooks: AgentHook[]): Promise<void> {
Expand Down Expand Up @@ -189,6 +196,71 @@ export class AgentHooksDistributor {
writeFileSync(hooksPath, JSON.stringify(config, null, 2), 'utf-8');
}

private async distributeFactoryHooks(hooks: AgentHook[]): Promise<void> {
// Factory uses the same hook format as Claude, stored in .factory/settings.json
const settingsPath = '.factory/settings.json';

// Load existing settings or create new
let settings: ClaudeSettings = {};
if (existsSync(settingsPath)) {
try {
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
} catch {
// Invalid JSON, start fresh
}
}

if (!settings.hooks) {
settings.hooks = {};
}

// Group hooks by event (Factory uses same events as Claude)
const hooksByEvent = new Map<ClaudeHookEvent, AgentHook[]>();

for (const hook of hooks) {
const factoryEvent = CLAUDE_EVENT_MAP[hook.event]; // Factory uses same event names
if (!factoryEvent) {
console.warn(`Event '${hook.event}' is not supported by Factory, skipping...`);
continue;
}

if (!hooksByEvent.has(factoryEvent)) {
hooksByEvent.set(factoryEvent, []);
}
hooksByEvent.get(factoryEvent)?.push(hook);
}

// Build Factory hooks config (same format as Claude)
for (const [event, eventHooks] of hooksByEvent) {
if (!settings.hooks[event]) {
settings.hooks[event] = [];
}

for (const hook of eventHooks) {
const command = this.buildCommand(hook);
const matcher = hook.matcher || CLAUDE_DEFAULT_MATCHERS[hook.event] || '*';

// Check if we already have a hook with this matcher
const existingEntry = settings.hooks[event].find(h => h.matcher === matcher);

if (existingEntry) {
// Add to existing matcher's hooks
existingEntry.hooks.push({ type: 'command', command });
} else {
// Create new entry
settings.hooks[event].push({
matcher,
hooks: [{ type: 'command', command }]
});
}
}
}

// Write settings
mkdirSync(dirname(settingsPath), { recursive: true });
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
}

private buildCommand(hook: AgentHook): string {
if (hook.command) {
return hook.command;
Expand Down Expand Up @@ -223,13 +295,17 @@ export class AgentHooksDistributor {

const hasClaudeHooks = this.config.hooks.some(h => h.targets.includes('claude'));
const hasCursorHooks = this.config.hooks.some(h => h.targets.includes('cursor'));
const hasFactoryHooks = this.config.hooks.some(h => h.targets.includes('factory'));

if (hasClaudeHooks) {
paths.push('.claude/settings.json');
}
if (hasCursorHooks) {
paths.push('.cursor/hooks.json');
}
if (hasFactoryHooks) {
paths.push('.factory/settings.json');
}

return paths;
}
Expand Down
13 changes: 12 additions & 1 deletion src/agents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ export const AGENT_DIRECTORY_MAPPINGS: Record<KnownDirectoryType, Partial<Record
claude: '.claude/commands',
cursor: '.cursor/commands',
opencode: '.opencode/command',
codex: '.codex/prompts', // Codex uses "prompts" for commands
},
skills: {
claude: '.claude/skills',
cursor: '.cursor/skills',
opencode: '.claude/skills', // opencode uses Claude-compatible path
codex: '.codex/skills',
factory: '.factory/skills',
},
agents: {
claude: '.claude/agents',
cursor: '.cursor/agents',
opencode: '.opencode/agent',
factory: '.factory/droids', // Factory uses "droids" for agents
},
};

Expand Down Expand Up @@ -50,7 +54,8 @@ export const AGENT_MAPPINGS: Record<AgentName, AgentMapping> = {
codex: {
path: 'AGENTS.md',
format: 'markdown',
mcpPath: 'codex_mcp.json'
directory: '.codex',
mcpPath: '.codex/config.toml'
},
roocode: {
path: '.roo/rules/{name}.md',
Expand All @@ -63,6 +68,12 @@ export const AGENT_MAPPINGS: Record<AgentName, AgentMapping> = {
format: 'markdown',
mcpPath: 'opencode.jsonc'
},
factory: {
path: 'AGENTS.md',
format: 'markdown',
directory: '.factory',
mcpPath: '.factory/settings.json'
},
generic: {
path: '{name}.md',
format: 'markdown',
Expand Down
Loading