Claude / scripts /setup-rtk.js
Cnass's picture
Upload 7 files
397ffab verified
Raw History Blame Contribute Delete
9.31 kB
#!/usr/bin/env node
// rtk (https://github.com/rtk-ai/rtk) shortens the output of shell commands
// before Claude reads it. Claude Code runs it through a PreToolUse hook on the
// Bash tool: `git status` becomes `rtk git status`. This script registers that
// hook in ~/.claude/settings.json and writes rtk's config; it runs at
// container start, before any Claude process.
//
// AGENTWEB_RTK=0 turns it off (the hook is removed). Removing the hook by hand
// (or `rtk init -g --uninstall`) is respected; the config file is only
// rewritten while it is still exactly what this script wrote.
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const HOOK_COMMAND = 'rtk hook claude';
const HOOK_ENTRY = { matcher: 'Bash', hooks: [{ type: 'command', command: HOOK_COMMAND }] };
// Safe mode: no information loss. Most rtk filters summarise (test failures
// cut to a few lines, `head -N` turned into a "smart" excerpt, lists capped,
// git messages replaced by "ok"), and only some keep the full output. So every
// command rtk would rewrite is excluded except the ones whose rtk output was
// checked to carry the same information: `git status` (porcelain listing,
// nothing capped with the limits below), `git add`, and npm/npx script runs
// (only npm's own banner, notices and warnings go; the program's output stays).
//
// rtk ignores the whole file when a section misses a field it requires (and
// then rewrites everything with its defaults), so [filters], [telemetry] and
// [limits] are always written complete.
const GIT_EXCLUDED = ['log', 'show', 'diff', 'stash', 'blame', 'commit', 'push', 'pull', 'fetch', 'checkout', 'branch', 'worktree'];
const EXCLUDED = [
// git commands with output worth every line (also as `git -C <dir> ...`)
...GIT_EXCLUDED.map((sub) => `git ${sub}`),
`^git\\s+((-C|-c)\\s+\\S+\\s+|--no-pager\\s+|--git-dir=\\S+\\s+|--work-tree=\\S+\\s+)*(${GIT_EXCLUDED.join('|')})(\\s|$)`,
'yadm', 'gh', 'glab', 'gt',
// reading, searching, listing
'cat', 'head', 'tail', 'grep', 'rg', 'ast-grep', 'find', 'ls', 'tree', 'diff', 'wc', 'du', 'df', 'ps',
// downloads, APIs, databases, infrastructure
'curl', 'wget', 'psql', 'aws', 'gcloud', 'kubectl', 'oc', 'helm', 'docker', 'terraform', 'tofu', 'pulumi',
'ansible-playbook', 'systemctl', 'iptables', 'fail2ban-client', 'sops', 'ping', 'rsync', 'liquibase',
// tests, linters, type checkers, builds (their filters cut error details)
'tsc', 'biome', 'eslint', 'lint', 'prettier', 'next', 'jest', 'vitest', 'ctest', 'playwright', 'prisma',
'mypy', 'ruff', 'sqlfluff', 'pytest', 'go', 'golangci-lint', 'golangci', 'cargo', 'make', 'sbt', 'mix',
'swift', 'dotnet', 'mvn', 'mvnw', './mvnw', 'mvnd', 'gradle', 'gradlew', './gradlew', 'pio', 'trunk', 'quarto',
'rspec', 'rubocop', 'rake', 'rails', 'bundle', 'php', 'phpunit', 'phpstan', 'pest', 'paratest', 'ecs', 'pint',
'shellcheck', 'yamllint', 'markdownlint', 'hadolint', 'pre-commit', 'shopify',
// package managers whose filters cap or cut output
'pip', 'pip3', 'uv', 'poetry', 'pnpm', 'bun', 'bunx', 'deno', 'brew', 'composer',
];
const CONFIG = `# Agent Web tarafindan yazildi (scripts/setup-rtk.js). Degistirirsen
# dokunulmaz; silersen bir sonraki baslatmada yeniden yazilir.
#
# Guvenli mod (bilgi kaybi yok): rtk'nin cogu filtresi ozet cikarir (test
# hatalarini birkac satira indirir, listeleri sinirlar, git mesajlarini "ok"
# yapar). Bu yuzden yalnizca ciktisi ayni bilgiyi tasiyan komutlar acik:
# git status, git add ve npm/npx ile script calistirma (npm'in kendi baslik,
# uyari ve bildirim satirlari gider; programin ciktisi aynen kalir).
# Daha fazla tasarruf icin asagidaki listeden komut cikarabilirsin; o
# komutlarin ciktisi rtk'nin ozetine doner.
[filters]
ignore_dirs = []
ignore_files = []
[limits]
grep_max_results = 1000000
grep_max_per_file = 1000000
status_max_files = 1000000
status_max_untracked = 1000000
passthrough_max_chars = 100000000
[retriever]
mode = "sqlite"
[telemetry]
enabled = false
[hooks]
exclude_commands = [
${EXCLUDED.map((c) => ` ${c.startsWith('^') ? `'${c}'` : JSON.stringify(c)},`).join('\n')}
]
`;
function log(message) {
console.log(`[setup-rtk] ${message}`);
}
function sha256(text) {
return crypto.createHash('sha256').update(text).digest('hex');
}
function findOnPath(name, envPath) {
for (const dir of String(envPath || '').split(':')) {
if (!dir) continue;
const candidate = path.join(dir, name);
try {
fs.accessSync(candidate, fs.constants.X_OK);
if (fs.statSync(candidate).isFile()) return candidate;
} catch {
// bu dizinde yok
}
}
return null;
}
function isRtkHook(command) {
return typeof command === 'string' && /(^|\/)rtk hook claude\s*$/.test(command.trim());
}
function hasHook(settings) {
const list = settings && settings.hooks && Array.isArray(settings.hooks.PreToolUse) ? settings.hooks.PreToolUse : [];
return list.some((entry) => entry && Array.isArray(entry.hooks) && entry.hooks.some((h) => h && isRtkHook(h.command)));
}
function removeHook(settings) {
const list = settings.hooks && Array.isArray(settings.hooks.PreToolUse) ? settings.hooks.PreToolUse : [];
const kept = [];
for (const entry of list) {
if (!entry || !Array.isArray(entry.hooks)) {
kept.push(entry);
continue;
}
const hooks = entry.hooks.filter((h) => !(h && isRtkHook(h.command)));
if (hooks.length) kept.push({ ...entry, hooks });
}
if (kept.length) settings.hooks.PreToolUse = kept;
else delete settings.hooks.PreToolUse;
if (settings.hooks && !Object.keys(settings.hooks).length) delete settings.hooks;
}
function readJsonFile(file, fallback) {
if (!fs.existsSync(file)) return { value: fallback, mode: 0o600 };
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
return { value, mode: fs.statSync(file).mode & 0o777 };
}
function writeFileAtomic(file, text, mode) {
fs.mkdirSync(path.dirname(file), { recursive: true });
const tmp = `${file}.agentweb-tmp`;
fs.writeFileSync(tmp, text, { mode });
fs.renameSync(tmp, file);
fs.chmodSync(file, mode);
}
function apply(home, env = process.env) {
const settingsFile = path.join(home, '.claude', 'settings.json');
const stateFile = path.join(home, '.cc-web', 'managed-rtk.json');
const configFile = path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'rtk', 'config.toml');
let state = {};
try {
state = JSON.parse(fs.readFileSync(stateFile, 'utf8')) || {};
} catch {
state = {};
}
const before = JSON.stringify(state);
const setting = String(env.AGENTWEB_RTK || '').trim().toLowerCase();
const enabled = !['0', 'off', 'false', 'no'].includes(setting);
const installed = !!findOnPath('rtk', env.PATH);
let settings;
let mode;
try {
({ value: settings, mode } = readJsonFile(settingsFile, {}));
} catch (err) {
log(`~/.claude/settings.json okunamadi (${err.message}); dokunulmadi.`);
return;
}
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
log('~/.claude/settings.json beklenmeyen bicimde; dokunulmadi.');
return;
}
const present = hasHook(settings);
let settingsChanged = false;
if (!enabled || !installed) {
// Only a hook this script added is taken out again.
if (state.hook === true && present) {
removeHook(settings);
settingsChanged = true;
log(enabled ? 'rtk bulunamadi; hook kaldirildi.' : 'AGENTWEB_RTK=0: rtk hook kaldirildi.');
}
delete state.hook;
} else if (state.hook === undefined) {
if (!present) {
if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) settings.hooks = {};
if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
settings.hooks.PreToolUse.push(HOOK_ENTRY);
settingsChanged = true;
log('rtk hook tanimlandi (Bash ciktilari kisaltilir).');
}
state.hook = true;
}
// state.hook === true and the hook is gone: removed on purpose, respected.
if (settingsChanged) {
const backup = `${settingsFile}.agentweb-backup`;
if (fs.existsSync(settingsFile) && !fs.existsSync(backup)) fs.copyFileSync(settingsFile, backup);
writeFileAtomic(settingsFile, `${JSON.stringify(settings, null, 2)}\n`, mode);
}
if (enabled && installed) {
const wanted = sha256(CONFIG);
let current = null;
try {
current = sha256(fs.readFileSync(configFile, 'utf8'));
} catch {
current = null;
}
if (current === null || (current === state.config && current !== wanted)) {
writeFileAtomic(configFile, CONFIG, 0o644);
state.config = wanted;
log(current === null ? 'rtk ayarlari yazildi.' : 'rtk ayarlari guncellendi.');
} else if (current !== wanted && current !== state.config) {
// the user's own config; left alone
} else if (current === wanted && state.config !== wanted) {
state.config = wanted;
}
}
if (JSON.stringify(state) !== before) writeFileAtomic(stateFile, `${JSON.stringify(state, null, 2)}\n`, 0o600);
}
if (require.main === module) {
if (!process.env.HOME) {
log('HOME tanimli degil; atlandi.');
} else {
apply(process.env.HOME);
}
}
module.exports = { apply, CONFIG, HOOK_COMMAND, EXCLUDED };