@@ -0,0 +1,26 @@
|
||||
project_doc_max_bytes = 65536
|
||||
|
||||
[features]
|
||||
hooks = true
|
||||
|
||||
[agents]
|
||||
max_threads = 6
|
||||
max_depth = 1
|
||||
|
||||
[mcp_servers.tavily_search]
|
||||
url = "https://mcp.tavily.com/mcp/?tavilyApiKey=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"
|
||||
enabled = true
|
||||
http_headers = { Authorization = "Bearer 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" }
|
||||
|
||||
[mcp_servers.tavily_search.tools.tavily_extract]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.tavily_search.tools.tavily_search]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.context7]
|
||||
url = "https://mcp.context7.com/mcp"
|
||||
enabled = true
|
||||
|
||||
[mcp_servers.context7.http_headers]
|
||||
CONTEXT7_API_KEY = "ctx7sk-57f06da8-ba55-44d4-9b71-ce8e1640b0e2"
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/snapshot_ts_changes.mjs\"",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/format_ts_changes.mjs\"",
|
||||
"timeout": 30,
|
||||
"statusMessage": "Formatting TypeScript edits"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { existsSync, readFileSync, rmSync, statSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
const readInput = () => {
|
||||
try {
|
||||
const rawInput = readFileSync(0, 'utf8').trim();
|
||||
return rawInput ? JSON.parse(rawInput) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const runGit = (args) => {
|
||||
const result = spawnSync('git', args, { encoding: 'utf8' });
|
||||
return result.status === 0 ? result.stdout.split('\n').filter(Boolean) : [];
|
||||
};
|
||||
|
||||
const runCommand = (command, args) =>
|
||||
spawnSync(command, args, {
|
||||
encoding: 'utf8',
|
||||
shell: false,
|
||||
});
|
||||
|
||||
const getStatePath = (input) => {
|
||||
const turnId = String(input.turn_id ?? 'unknown').replace(/[^a-zA-Z0-9_.-]/g, '_');
|
||||
const cwdHash = createHash('sha256').update(process.cwd()).digest('hex').slice(0, 16);
|
||||
return join(tmpdir(), 'codex-fastboard-hooks', cwdHash, `${turnId}.json`);
|
||||
};
|
||||
|
||||
const isTypeScriptSource = (file) => /^src\/.+\.tsx?$/.test(file);
|
||||
|
||||
const getDirtyTypeScriptFiles = () => {
|
||||
const files = [
|
||||
...runGit(['diff', '--name-only', '--diff-filter=ACMR']),
|
||||
...runGit(['diff', '--cached', '--name-only', '--diff-filter=ACMR']),
|
||||
...runGit(['ls-files', '--others', '--exclude-standard']),
|
||||
];
|
||||
|
||||
return [...new Set(files)].filter(isTypeScriptSource).sort();
|
||||
};
|
||||
|
||||
const getFileState = (file) => {
|
||||
try {
|
||||
const stat = statSync(file);
|
||||
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const hasChangedSinceSnapshot = (beforeState, file) => {
|
||||
const before = beforeState[file];
|
||||
const current = getFileState(file);
|
||||
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !before || before.mtimeMs !== current.mtimeMs || before.size !== current.size;
|
||||
};
|
||||
|
||||
const firstLines = (value, maxLines = 30) =>
|
||||
value
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.slice(0, maxLines)
|
||||
.join('\n');
|
||||
|
||||
const report = (message) => {
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
continue: true,
|
||||
systemMessage: message,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const input = readInput();
|
||||
const statePath = getStatePath(input);
|
||||
const beforeState = existsSync(statePath) ? JSON.parse(readFileSync(statePath, 'utf8')) : {};
|
||||
const files = getDirtyTypeScriptFiles().filter((file) => hasChangedSinceSnapshot(beforeState, file));
|
||||
|
||||
if (existsSync(statePath)) {
|
||||
rmSync(statePath, { force: true });
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const prettier = runCommand('npx', ['prettier', '--write', ...files]);
|
||||
|
||||
if (prettier.status !== 0) {
|
||||
report(`Prettier hook failed:\n${firstLines(`${prettier.stdout}\n${prettier.stderr}`)}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const eslint = runCommand('npx', [
|
||||
'eslint',
|
||||
'--no-error-on-unmatched-pattern',
|
||||
'--max-warnings=0',
|
||||
...files,
|
||||
]);
|
||||
|
||||
if (eslint.status !== 0) {
|
||||
report(`ESLint hook found issues:\n${firstLines(`${eslint.stdout}\n${eslint.stderr}`)}`);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
const readInput = () => {
|
||||
try {
|
||||
const rawInput = readFileSync(0, 'utf8').trim();
|
||||
return rawInput ? JSON.parse(rawInput) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const runGit = (args) => {
|
||||
const result = spawnSync('git', args, { encoding: 'utf8' });
|
||||
return result.status === 0 ? result.stdout.split('\n').filter(Boolean) : [];
|
||||
};
|
||||
|
||||
const getStatePath = (input) => {
|
||||
const turnId = String(input.turn_id ?? 'unknown').replace(/[^a-zA-Z0-9_.-]/g, '_');
|
||||
const cwdHash = createHash('sha256').update(process.cwd()).digest('hex').slice(0, 16);
|
||||
return join(tmpdir(), 'codex-fastboard-hooks', cwdHash, `${turnId}.json`);
|
||||
};
|
||||
|
||||
const isTypeScriptSource = (file) => /^src\/.+\.tsx?$/.test(file);
|
||||
|
||||
const getDirtyTypeScriptFiles = () => {
|
||||
const files = [
|
||||
...runGit(['diff', '--name-only', '--diff-filter=ACMR']),
|
||||
...runGit(['diff', '--cached', '--name-only', '--diff-filter=ACMR']),
|
||||
...runGit(['ls-files', '--others', '--exclude-standard']),
|
||||
];
|
||||
|
||||
return [...new Set(files)].filter(isTypeScriptSource).sort();
|
||||
};
|
||||
|
||||
const getFileState = (file) => {
|
||||
try {
|
||||
const stat = statSync(file);
|
||||
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const input = readInput();
|
||||
const statePath = getStatePath(input);
|
||||
const snapshot = Object.fromEntries(
|
||||
getDirtyTypeScriptFiles().map((file) => [file, getFileState(file)]),
|
||||
);
|
||||
|
||||
mkdirSync(dirname(statePath), { recursive: true });
|
||||
writeFileSync(statePath, JSON.stringify(snapshot, null, 2));
|
||||
Reference in New Issue
Block a user