mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
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:
62
.cursor/hooks/adapter.js
Normal file
62
.cursor/hooks/adapter.js
Normal 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 };
|
||||
17
.cursor/hooks/after-file-edit.js
Normal file
17
.cursor/hooks/after-file-edit.js
Normal 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));
|
||||
12
.cursor/hooks/after-mcp-execution.js
Normal file
12
.cursor/hooks/after-mcp-execution.js
Normal 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));
|
||||
26
.cursor/hooks/after-shell-execution.js
Normal file
26
.cursor/hooks/after-shell-execution.js
Normal 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));
|
||||
12
.cursor/hooks/after-tab-file-edit.js
Normal file
12
.cursor/hooks/after-tab-file-edit.js
Normal 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));
|
||||
11
.cursor/hooks/before-mcp-execution.js
Normal file
11
.cursor/hooks/before-mcp-execution.js
Normal 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));
|
||||
13
.cursor/hooks/before-read-file.js
Normal file
13
.cursor/hooks/before-read-file.js
Normal 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));
|
||||
27
.cursor/hooks/before-shell-execution.js
Normal file
27
.cursor/hooks/before-shell-execution.js
Normal 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));
|
||||
23
.cursor/hooks/before-submit-prompt.js
Normal file
23
.cursor/hooks/before-submit-prompt.js
Normal 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));
|
||||
13
.cursor/hooks/before-tab-file-read.js
Normal file
13
.cursor/hooks/before-tab-file-read.js
Normal 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));
|
||||
7
.cursor/hooks/pre-compact.js
Normal file
7
.cursor/hooks/pre-compact.js
Normal 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));
|
||||
9
.cursor/hooks/session-end.js
Normal file
9
.cursor/hooks/session-end.js
Normal 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));
|
||||
8
.cursor/hooks/session-start.js
Normal file
8
.cursor/hooks/session-start.js
Normal 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
7
.cursor/hooks/stop.js
Normal 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));
|
||||
10
.cursor/hooks/subagent-start.js
Normal file
10
.cursor/hooks/subagent-start.js
Normal 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));
|
||||
10
.cursor/hooks/subagent-stop.js
Normal file
10
.cursor/hooks/subagent-stop.js
Normal 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));
|
||||
Reference in New Issue
Block a user