feat: add Cursor, Codex, and OpenCode harnesses — maximize every AI coding tool

- AGENTS.md: universal cross-tool file read by Claude Code, Cursor, Codex, and OpenCode
- .cursor/: 15 hook events via hooks.json, 16 hook scripts with DRY adapter pattern,
  29 rules (9 common + 20 language-specific) with Cursor YAML frontmatter
- .codex/: reference config.toml, Codex-specific AGENTS.md supplement,
  10 skills ported to .agents/skills/ with openai.yaml metadata
- .opencode/: 3 new tools (format-code, lint-check, git-summary), 3 new hooks
  (shell.env, experimental.session.compacting, permission.ask), expanded instructions,
  version bumped to 1.6.0
- README: fixed Cursor section, added Codex section, added cross-tool parity table
- install.sh: now copies hooks.json + hooks/ for --target cursor
This commit is contained in:
Affaan Mustafa
2026-02-25 10:45:29 -08:00
parent a9b104fc23
commit d70bab85e3
83 changed files with 6249 additions and 212 deletions

62
.cursor/hooks/adapter.js Normal file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env node
/**
* Cursor-to-Claude Code Hook Adapter
* Transforms Cursor stdin JSON to Claude Code hook format,
* then delegates to existing scripts/hooks/*.js
*/
const { execFileSync } = require('child_process');
const path = require('path');
const MAX_STDIN = 1024 * 1024;
function readStdin() {
return new Promise((resolve) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (data.length < MAX_STDIN) data += chunk.substring(0, MAX_STDIN - data.length);
});
process.stdin.on('end', () => resolve(data));
});
}
function getPluginRoot() {
return path.resolve(__dirname, '..', '..');
}
function transformToClaude(cursorInput, overrides = {}) {
return {
tool_input: {
command: cursorInput.command || cursorInput.args?.command || '',
file_path: cursorInput.path || cursorInput.file || '',
...overrides.tool_input,
},
tool_output: {
output: cursorInput.output || cursorInput.result || '',
...overrides.tool_output,
},
_cursor: {
conversation_id: cursorInput.conversation_id,
hook_event_name: cursorInput.hook_event_name,
workspace_roots: cursorInput.workspace_roots,
model: cursorInput.model,
},
};
}
function runExistingHook(scriptName, stdinData) {
const scriptPath = path.join(getPluginRoot(), 'scripts', 'hooks', scriptName);
try {
execFileSync('node', [scriptPath], {
input: typeof stdinData === 'string' ? stdinData : JSON.stringify(stdinData),
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 15000,
cwd: process.cwd(),
});
} catch (e) {
if (e.status === 2) process.exit(2); // Forward blocking exit code
}
}
module.exports = { readStdin, getPluginRoot, transformToClaude, runExistingHook };

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env node
const { readStdin, runExistingHook, transformToClaude } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
const claudeInput = transformToClaude(input, {
tool_input: { file_path: input.path || input.file || '' }
});
const claudeStr = JSON.stringify(claudeInput);
// Run format, typecheck, and console.log warning sequentially
runExistingHook('post-edit-format.js', claudeStr);
runExistingHook('post-edit-typecheck.js', claudeStr);
runExistingHook('post-edit-console-warn.js', claudeStr);
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env node
const { readStdin } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
const server = input.server || input.mcp_server || 'unknown';
const tool = input.tool || input.mcp_tool || 'unknown';
const success = input.error ? 'FAILED' : 'OK';
console.error(`[ECC] MCP result: ${server}/${tool} - ${success}`);
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env node
const { readStdin } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
const cmd = input.command || '';
const output = input.output || input.result || '';
// PR creation logging
if (/gh pr create/.test(cmd)) {
const m = output.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/);
if (m) {
console.error('[ECC] PR created: ' + m[0]);
const repo = m[0].replace(/https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/, '$1');
const pr = m[0].replace(/.+\/pull\/(\d+)/, '$1');
console.error('[ECC] To review: gh pr review ' + pr + ' --repo ' + repo);
}
}
// Build completion notice
if (/(npm run build|pnpm build|yarn build)/.test(cmd)) {
console.error('[ECC] Build completed');
}
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env node
const { readStdin, runExistingHook, transformToClaude } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
const claudeInput = transformToClaude(input, {
tool_input: { file_path: input.path || input.file || '' }
});
runExistingHook('post-edit-format.js', JSON.stringify(claudeInput));
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env node
const { readStdin } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
const server = input.server || input.mcp_server || 'unknown';
const tool = input.tool || input.mcp_tool || 'unknown';
console.error(`[ECC] MCP invocation: ${server}/${tool}`);
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env node
const { readStdin } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
const filePath = input.path || input.file || '';
if (/\.(env|key|pem)$|\.env\.|credentials|secret/i.test(filePath)) {
console.error('[ECC] WARNING: Reading sensitive file: ' + filePath);
console.error('[ECC] Ensure this data is not exposed in outputs');
}
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env node
const { readStdin } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
const cmd = input.command || '';
// 1. Block dev server outside tmux
if (process.platform !== 'win32' && /(npm run dev\b|pnpm( run)? dev\b|yarn dev\b|bun run dev\b)/.test(cmd)) {
console.error('[ECC] BLOCKED: Dev server must run in tmux for log access');
console.error('[ECC] Use: tmux new-session -d -s dev "npm run dev"');
process.exit(2);
}
// 2. Tmux reminder for long-running commands
if (process.platform !== 'win32' && !process.env.TMUX &&
/(npm (install|test)|pnpm (install|test)|yarn (install|test)?|bun (install|test)|cargo build|make\b|docker\b|pytest|vitest|playwright)/.test(cmd)) {
console.error('[ECC] Consider running in tmux for session persistence');
}
// 3. Git push review reminder
if (/git push/.test(cmd)) {
console.error('[ECC] Review changes before push: git diff origin/main...HEAD');
}
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env node
const { readStdin } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
const prompt = input.prompt || input.content || input.message || '';
const secretPatterns = [
/sk-[a-zA-Z0-9]{20,}/, // OpenAI API keys
/ghp_[a-zA-Z0-9]{36,}/, // GitHub personal access tokens
/AKIA[A-Z0-9]{16}/, // AWS access keys
/xox[bpsa]-[a-zA-Z0-9-]+/, // Slack tokens
/-----BEGIN (RSA |EC )?PRIVATE KEY-----/, // Private keys
];
for (const pattern of secretPatterns) {
if (pattern.test(prompt)) {
console.error('[ECC] WARNING: Potential secret detected in prompt!');
console.error('[ECC] Remove secrets before submitting. Use environment variables instead.');
break;
}
}
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env node
const { readStdin } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
const filePath = input.path || input.file || '';
if (/\.(env|key|pem)$|\.env\.|credentials|secret/i.test(filePath)) {
console.error('[ECC] BLOCKED: Tab cannot read sensitive file: ' + filePath);
process.exit(2);
}
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env node
const { readStdin, runExistingHook, transformToClaude } = require('./adapter');
readStdin().then(raw => {
const claudeInput = JSON.parse(raw || '{}');
runExistingHook('pre-compact.js', transformToClaude(claudeInput));
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env node
const { readStdin, runExistingHook, transformToClaude } = require('./adapter');
readStdin().then(raw => {
const input = JSON.parse(raw);
const claudeInput = transformToClaude(input);
runExistingHook('session-end.js', claudeInput);
runExistingHook('evaluate-session.js', claudeInput);
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env node
const { readStdin, runExistingHook, transformToClaude } = require('./adapter');
readStdin().then(raw => {
const input = JSON.parse(raw);
const claudeInput = transformToClaude(input);
runExistingHook('session-start.js', claudeInput);
process.stdout.write(raw);
}).catch(() => process.exit(0));

7
.cursor/hooks/stop.js Normal file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env node
const { readStdin, runExistingHook, transformToClaude } = require('./adapter');
readStdin().then(raw => {
const claudeInput = JSON.parse(raw || '{}');
runExistingHook('check-console-log.js', transformToClaude(claudeInput));
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env node
const { readStdin } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
const agent = input.agent_name || input.agent || 'unknown';
console.error(`[ECC] Agent spawned: ${agent}`);
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env node
const { readStdin } = require('./adapter');
readStdin().then(raw => {
try {
const input = JSON.parse(raw);
const agent = input.agent_name || input.agent || 'unknown';
console.error(`[ECC] Agent completed: ${agent}`);
} catch {}
process.stdout.write(raw);
}).catch(() => process.exit(0));