kastell 2.2.4 → 2.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +18 -18
- package/.claude-plugin/plugin.json +45 -38
- package/CHANGELOG.md +1294 -1266
- package/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +1 -1
- package/README.tr.md +1 -1
- package/bin/kastell +2 -2
- package/bin/kastell-mcp +5 -5
- package/dist/adapters/coolify.js +92 -92
- package/dist/adapters/dokploy.js +99 -99
- package/dist/core/audit/formatters/badge.js +20 -20
- package/dist/core/completions.js +631 -631
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +25 -31
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/serverExplain.d.ts.map +1 -1
- package/dist/mcp/tools/serverExplain.js.map +1 -1
- package/dist/mcp/tools/serverFleet.d.ts.map +1 -1
- package/dist/mcp/tools/serverFleet.js.map +1 -1
- package/dist/mcp/tools/serverInfo.d.ts +1 -1
- package/dist/mcp/tools/serverInfo.js +1 -1
- package/dist/mcp/tools/serverPlugin.d.ts.map +1 -1
- package/dist/mcp/tools/serverPlugin.js.map +1 -1
- package/dist/mcp-bundle.mjs +101015 -0
- package/dist/utils/cloudInit.js +58 -58
- package/dist/utils/version.d.ts.map +1 -1
- package/dist/utils/version.js +19 -4
- package/dist/utils/version.js.map +1 -1
- package/kastell-plugin/.claude-plugin/plugin.json +20 -20
- package/kastell-plugin/.mcp.json +15 -8
- package/kastell-plugin/README.md +113 -113
- package/kastell-plugin/agents/kastell-auditor.md +77 -77
- package/kastell-plugin/agents/scripts/bucket_mapper.sh +101 -101
- package/kastell-plugin/agents/scripts/trend_report.sh +91 -91
- package/kastell-plugin/hooks/destroy-block.cjs +31 -31
- package/kastell-plugin/hooks/hooks.json +57 -57
- package/kastell-plugin/hooks/pre-commit-audit-guard.cjs +75 -75
- package/kastell-plugin/hooks/session-audit.cjs +86 -86
- package/kastell-plugin/hooks/session-log.cjs +56 -56
- package/kastell-plugin/hooks/stop-quality-check.cjs +72 -72
- package/kastell-plugin/skills/kastell-careful/SKILL.md +64 -64
- package/kastell-plugin/skills/kastell-ops/SKILL.md +139 -139
- package/kastell-plugin/skills/kastell-ops/references/commands.md +45 -45
- package/kastell-plugin/skills/kastell-ops/references/mcp-tools.md +50 -50
- package/kastell-plugin/skills/kastell-ops/references/patterns.md +145 -145
- package/kastell-plugin/skills/kastell-ops/references/pitfalls.md +136 -136
- package/kastell-plugin/skills/kastell-ops/scripts/check_coverage.sh +101 -101
- package/kastell-plugin/skills/kastell-ops/scripts/fleet_report.sh +73 -73
- package/kastell-plugin/skills/kastell-ops/scripts/parse_audit.sh +76 -76
- package/kastell-plugin/skills/kastell-research/SKILL.md +90 -90
- package/kastell-plugin/skills/kastell-scaffold/SKILL.md +104 -104
- package/kastell-plugin/skills/kastell-scaffold/references/template-audit-check.md +150 -150
- package/kastell-plugin/skills/kastell-scaffold/references/template-command.md +80 -80
- package/kastell-plugin/skills/kastell-scaffold/references/template-mcp-tool.md +72 -72
- package/kastell-plugin/skills/kastell-scaffold/references/template-provider.md +67 -67
- package/kastell-plugin/skills/kastell-scaffold/scripts/scaffold.sh +180 -180
- package/kastell-plugin/skills/kastell-scaffold/templates/check-test.ts.tpl +27 -27
- package/kastell-plugin/skills/kastell-scaffold/templates/check.ts.tpl +50 -50
- package/kastell-plugin/skills/kastell-scaffold/templates/command-core.ts.tpl +18 -18
- package/kastell-plugin/skills/kastell-scaffold/templates/command-test.ts.tpl +17 -17
- package/kastell-plugin/skills/kastell-scaffold/templates/command.ts.tpl +25 -25
- package/kastell-plugin/skills/kastell-scaffold/templates/mcp-tool-test.ts.tpl +30 -30
- package/kastell-plugin/skills/kastell-scaffold/templates/mcp-tool.ts.tpl +29 -29
- package/kastell-plugin/skills/kastell-scaffold/templates/provider-test.ts.tpl +34 -34
- package/kastell-plugin/skills/kastell-scaffold/templates/provider.ts.tpl +32 -32
- package/package.json +125 -122
- package/dist/commands/interactive.d.ts +0 -11
- package/dist/commands/interactive.d.ts.map +0 -1
- package/dist/commands/interactive.js +0 -1079
- package/dist/commands/interactive.js.map +0 -1
- package/dist/core/lock.d.ts +0 -66
- package/dist/core/lock.d.ts.map +0 -1
- package/dist/core/lock.js +0 -556
- package/dist/core/lock.js.map +0 -1
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// PreToolUse hook: Block git commit if audit score dropped since previous audit run
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
|
|
8
|
-
const HISTORY_FILE = path.join(os.homedir(), '.kastell', 'audit-history.json');
|
|
9
|
-
|
|
10
|
-
// MANDATORY stdin guard — exit silently if stdin unavailable
|
|
11
|
-
if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
|
|
12
|
-
process.exit(0);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let input = '';
|
|
16
|
-
const stdinTimeout = setTimeout(() => process.exit(0), 1500);
|
|
17
|
-
process.stdin.setEncoding('utf8');
|
|
18
|
-
process.stdin.on('error', () => process.exit(0));
|
|
19
|
-
process.stdin.on('data', chunk => { input += chunk; });
|
|
20
|
-
process.stdin.on('end', () => {
|
|
21
|
-
clearTimeout(stdinTimeout);
|
|
22
|
-
try {
|
|
23
|
-
const cwd = process.cwd();
|
|
24
|
-
|
|
25
|
-
// Kastell project guard (two-phase: directory structure + package.json name)
|
|
26
|
-
const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
|
|
27
|
-
fs.existsSync(path.join(cwd, 'package.json'));
|
|
28
|
-
if (!isKastell) {
|
|
29
|
-
try {
|
|
30
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
31
|
-
if (pkg.name !== 'kastell') process.exit(0);
|
|
32
|
-
} catch { process.exit(0); }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Parse tool input — only act on git commit commands
|
|
36
|
-
const data = JSON.parse(input);
|
|
37
|
-
const cmd = (data.tool_input && data.tool_input.command) || '';
|
|
38
|
-
|
|
39
|
-
if (!/\bgit\s+commit\b/.test(cmd)) {
|
|
40
|
-
process.exit(0);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Read audit history — fail-open if unavailable
|
|
44
|
-
let history;
|
|
45
|
-
try {
|
|
46
|
-
history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
|
|
47
|
-
} catch {
|
|
48
|
-
// History missing or unreadable — allow commit
|
|
49
|
-
process.exit(0);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Check each server for score regression between last two audit runs
|
|
53
|
-
for (const serverIp of Object.keys(history)) {
|
|
54
|
-
const entries = history[serverIp];
|
|
55
|
-
if (!Array.isArray(entries) || entries.length < 2) continue;
|
|
56
|
-
|
|
57
|
-
const current = entries[entries.length - 1].overallScore;
|
|
58
|
-
const previous = entries[entries.length - 2].overallScore;
|
|
59
|
-
|
|
60
|
-
if (typeof current !== 'number' || typeof previous !== 'number') continue;
|
|
61
|
-
if (current < previous) {
|
|
62
|
-
process.stdout.write(JSON.stringify({
|
|
63
|
-
decision: 'block',
|
|
64
|
-
reason: `Audit score dropped: ${previous} -> ${current} (${entries[entries.length - 1].serverName}). Run \`kastell audit\` to investigate before committing.`,
|
|
65
|
-
}));
|
|
66
|
-
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// No score drop detected — allow commit
|
|
71
|
-
} catch {}
|
|
72
|
-
|
|
73
|
-
// Fail-open: any unexpected error allows the commit through
|
|
74
|
-
process.exit(0);
|
|
75
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse hook: Block git commit if audit score dropped since previous audit run
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const HISTORY_FILE = path.join(os.homedir(), '.kastell', 'audit-history.json');
|
|
9
|
+
|
|
10
|
+
// MANDATORY stdin guard — exit silently if stdin unavailable
|
|
11
|
+
if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let input = '';
|
|
16
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 1500);
|
|
17
|
+
process.stdin.setEncoding('utf8');
|
|
18
|
+
process.stdin.on('error', () => process.exit(0));
|
|
19
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
20
|
+
process.stdin.on('end', () => {
|
|
21
|
+
clearTimeout(stdinTimeout);
|
|
22
|
+
try {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
|
|
25
|
+
// Kastell project guard (two-phase: directory structure + package.json name)
|
|
26
|
+
const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
|
|
27
|
+
fs.existsSync(path.join(cwd, 'package.json'));
|
|
28
|
+
if (!isKastell) {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
31
|
+
if (pkg.name !== 'kastell') process.exit(0);
|
|
32
|
+
} catch { process.exit(0); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Parse tool input — only act on git commit commands
|
|
36
|
+
const data = JSON.parse(input);
|
|
37
|
+
const cmd = (data.tool_input && data.tool_input.command) || '';
|
|
38
|
+
|
|
39
|
+
if (!/\bgit\s+commit\b/.test(cmd)) {
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Read audit history — fail-open if unavailable
|
|
44
|
+
let history;
|
|
45
|
+
try {
|
|
46
|
+
history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
|
|
47
|
+
} catch {
|
|
48
|
+
// History missing or unreadable — allow commit
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check each server for score regression between last two audit runs
|
|
53
|
+
for (const serverIp of Object.keys(history)) {
|
|
54
|
+
const entries = history[serverIp];
|
|
55
|
+
if (!Array.isArray(entries) || entries.length < 2) continue;
|
|
56
|
+
|
|
57
|
+
const current = entries[entries.length - 1].overallScore;
|
|
58
|
+
const previous = entries[entries.length - 2].overallScore;
|
|
59
|
+
|
|
60
|
+
if (typeof current !== 'number' || typeof previous !== 'number') continue;
|
|
61
|
+
if (current < previous) {
|
|
62
|
+
process.stdout.write(JSON.stringify({
|
|
63
|
+
decision: 'block',
|
|
64
|
+
reason: `Audit score dropped: ${previous} -> ${current} (${entries[entries.length - 1].serverName}). Run \`kastell audit\` to investigate before committing.`,
|
|
65
|
+
}));
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// No score drop detected — allow commit
|
|
71
|
+
} catch {}
|
|
72
|
+
|
|
73
|
+
// Fail-open: any unexpected error allows the commit through
|
|
74
|
+
process.exit(0);
|
|
75
|
+
});
|
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SessionStart hook: Show last audit score from cache (no SSH, instant)
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
|
|
8
|
-
const HISTORY_FILE = path.join(os.homedir(), '.kastell', 'audit-history.json');
|
|
9
|
-
|
|
10
|
-
// MANDATORY stdin guard — exit silently if stdin unavailable (e.g. after /clear)
|
|
11
|
-
if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
|
|
12
|
-
process.exit(0);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let input = '';
|
|
16
|
-
const stdinTimeout = setTimeout(() => process.exit(0), 1500);
|
|
17
|
-
process.stdin.setEncoding('utf8');
|
|
18
|
-
process.stdin.on('error', () => process.exit(0));
|
|
19
|
-
process.stdin.on('data', chunk => { input += chunk; });
|
|
20
|
-
process.stdin.on('end', () => {
|
|
21
|
-
clearTimeout(stdinTimeout);
|
|
22
|
-
try {
|
|
23
|
-
const cwd = process.cwd();
|
|
24
|
-
|
|
25
|
-
// Kastell project guard
|
|
26
|
-
const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
|
|
27
|
-
fs.existsSync(path.join(cwd, 'package.json'));
|
|
28
|
-
if (!isKastell) {
|
|
29
|
-
try {
|
|
30
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
31
|
-
if (pkg.name !== 'kastell') process.exit(0);
|
|
32
|
-
} catch { process.exit(0); }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Read audit history from cache — no SSH needed
|
|
36
|
-
let history;
|
|
37
|
-
try {
|
|
38
|
-
history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
|
|
39
|
-
} catch {
|
|
40
|
-
// No history file — exit silently
|
|
41
|
-
process.exit(0);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Find the most recent audit entry across all servers
|
|
45
|
-
let latest = null;
|
|
46
|
-
let latestTime = 0;
|
|
47
|
-
|
|
48
|
-
if (typeof history === 'object' && !Array.isArray(history)) {
|
|
49
|
-
// Format: { "ip": [ { overallScore, serverName, timestamp } ] }
|
|
50
|
-
for (const entries of Object.values(history)) {
|
|
51
|
-
if (!Array.isArray(entries)) continue;
|
|
52
|
-
for (const entry of entries) {
|
|
53
|
-
const ts = new Date(entry.timestamp || entry.date || 0).getTime();
|
|
54
|
-
if (ts > latestTime) {
|
|
55
|
-
latestTime = ts;
|
|
56
|
-
latest = entry;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
} else if (Array.isArray(history)) {
|
|
61
|
-
// Format: [ { overallScore, serverName, timestamp } ]
|
|
62
|
-
for (const entry of history) {
|
|
63
|
-
const ts = new Date(entry.timestamp || entry.date || 0).getTime();
|
|
64
|
-
if (ts > latestTime) {
|
|
65
|
-
latestTime = ts;
|
|
66
|
-
latest = entry;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (latest && typeof latest.overallScore === 'number') {
|
|
72
|
-
const serverName = latest.serverName || latest.server || 'unknown';
|
|
73
|
-
const date = (latest.timestamp || latest.date || '').split('T')[0];
|
|
74
|
-
const ageMs = Date.now() - latestTime;
|
|
75
|
-
const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
76
|
-
const stale = ageDays > 7 ? ` (${ageDays} days ago — consider re-running)` : '';
|
|
77
|
-
|
|
78
|
-
process.stdout.write(JSON.stringify({
|
|
79
|
-
hookSpecificOutput: `[Kastell Audit] Last score: ${latest.overallScore}/100 (${serverName}, ${date})${stale}`,
|
|
80
|
-
}));
|
|
81
|
-
}
|
|
82
|
-
} catch {}
|
|
83
|
-
|
|
84
|
-
// Always exit 0 — SessionStart MUST NOT fail
|
|
85
|
-
process.exit(0);
|
|
86
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SessionStart hook: Show last audit score from cache (no SSH, instant)
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const HISTORY_FILE = path.join(os.homedir(), '.kastell', 'audit-history.json');
|
|
9
|
+
|
|
10
|
+
// MANDATORY stdin guard — exit silently if stdin unavailable (e.g. after /clear)
|
|
11
|
+
if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let input = '';
|
|
16
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 1500);
|
|
17
|
+
process.stdin.setEncoding('utf8');
|
|
18
|
+
process.stdin.on('error', () => process.exit(0));
|
|
19
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
20
|
+
process.stdin.on('end', () => {
|
|
21
|
+
clearTimeout(stdinTimeout);
|
|
22
|
+
try {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
|
|
25
|
+
// Kastell project guard
|
|
26
|
+
const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
|
|
27
|
+
fs.existsSync(path.join(cwd, 'package.json'));
|
|
28
|
+
if (!isKastell) {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
31
|
+
if (pkg.name !== 'kastell') process.exit(0);
|
|
32
|
+
} catch { process.exit(0); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Read audit history from cache — no SSH needed
|
|
36
|
+
let history;
|
|
37
|
+
try {
|
|
38
|
+
history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
|
|
39
|
+
} catch {
|
|
40
|
+
// No history file — exit silently
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Find the most recent audit entry across all servers
|
|
45
|
+
let latest = null;
|
|
46
|
+
let latestTime = 0;
|
|
47
|
+
|
|
48
|
+
if (typeof history === 'object' && !Array.isArray(history)) {
|
|
49
|
+
// Format: { "ip": [ { overallScore, serverName, timestamp } ] }
|
|
50
|
+
for (const entries of Object.values(history)) {
|
|
51
|
+
if (!Array.isArray(entries)) continue;
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const ts = new Date(entry.timestamp || entry.date || 0).getTime();
|
|
54
|
+
if (ts > latestTime) {
|
|
55
|
+
latestTime = ts;
|
|
56
|
+
latest = entry;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else if (Array.isArray(history)) {
|
|
61
|
+
// Format: [ { overallScore, serverName, timestamp } ]
|
|
62
|
+
for (const entry of history) {
|
|
63
|
+
const ts = new Date(entry.timestamp || entry.date || 0).getTime();
|
|
64
|
+
if (ts > latestTime) {
|
|
65
|
+
latestTime = ts;
|
|
66
|
+
latest = entry;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (latest && typeof latest.overallScore === 'number') {
|
|
72
|
+
const serverName = latest.serverName || latest.server || 'unknown';
|
|
73
|
+
const date = (latest.timestamp || latest.date || '').split('T')[0];
|
|
74
|
+
const ageMs = Date.now() - latestTime;
|
|
75
|
+
const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
76
|
+
const stale = ageDays > 7 ? ` (${ageDays} days ago — consider re-running)` : '';
|
|
77
|
+
|
|
78
|
+
process.stdout.write(JSON.stringify({
|
|
79
|
+
hookSpecificOutput: `[Kastell Audit] Last score: ${latest.overallScore}/100 (${serverName}, ${date})${stale}`,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
|
|
84
|
+
// Always exit 0 — SessionStart MUST NOT fail
|
|
85
|
+
process.exit(0);
|
|
86
|
+
});
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// PostToolUse hook: Log Bash command + output to ~/.kastell/session.log
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
|
|
8
|
-
const LOG_DIR = path.join(os.homedir(), '.kastell');
|
|
9
|
-
const LOG_FILE = path.join(LOG_DIR, 'session.log');
|
|
10
|
-
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB rotation threshold
|
|
11
|
-
const SECRET_PATTERN = /(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL|BEARER|AUTHORIZATION)\s*[=:]\s*\S+/gi;
|
|
12
|
-
|
|
13
|
-
// MANDATORY stdin guard — exit silently if stdin unavailable
|
|
14
|
-
if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
|
|
15
|
-
process.exit(0);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let input = '';
|
|
19
|
-
const stdinTimeout = setTimeout(() => process.exit(0), 1500);
|
|
20
|
-
process.stdin.setEncoding('utf8');
|
|
21
|
-
process.stdin.on('error', () => process.exit(0));
|
|
22
|
-
process.stdin.on('data', chunk => { input += chunk; });
|
|
23
|
-
process.stdin.on('end', () => {
|
|
24
|
-
clearTimeout(stdinTimeout);
|
|
25
|
-
try {
|
|
26
|
-
const data = JSON.parse(input);
|
|
27
|
-
|
|
28
|
-
// Safety check — matcher already filters, but guard explicitly
|
|
29
|
-
if (data.tool_name !== 'Bash') {
|
|
30
|
-
process.exit(0);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const cmd = ((data.tool_input && data.tool_input.command) || '').substring(0, 500);
|
|
34
|
-
const rawOut = ((data.tool_response && data.tool_response.output) || '').substring(0, 2000);
|
|
35
|
-
const out = rawOut.replace(SECRET_PATTERN, '[REDACTED]');
|
|
36
|
-
|
|
37
|
-
const timestamp = new Date().toISOString();
|
|
38
|
-
const entry = `[${timestamp}] CMD: ${cmd}\nOUT: ${out}\n---\n`;
|
|
39
|
-
|
|
40
|
-
// Ensure log directory exists (guard avoids redundant mkdirSync on hot path)
|
|
41
|
-
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
42
|
-
|
|
43
|
-
// Log rotation: truncate when exceeding threshold
|
|
44
|
-
try {
|
|
45
|
-
const stat = fs.statSync(LOG_FILE);
|
|
46
|
-
if (stat.size > MAX_LOG_SIZE) fs.writeFileSync(LOG_FILE, entry, { mode: 0o600 });
|
|
47
|
-
else fs.appendFileSync(LOG_FILE, entry, 'utf8');
|
|
48
|
-
} catch {
|
|
49
|
-
// File does not exist yet — create with restrictive permissions
|
|
50
|
-
fs.writeFileSync(LOG_FILE, entry, { mode: 0o600 });
|
|
51
|
-
}
|
|
52
|
-
} catch {}
|
|
53
|
-
|
|
54
|
-
// Always exit 0 — logging must never block execution
|
|
55
|
-
process.exit(0);
|
|
56
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PostToolUse hook: Log Bash command + output to ~/.kastell/session.log
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const LOG_DIR = path.join(os.homedir(), '.kastell');
|
|
9
|
+
const LOG_FILE = path.join(LOG_DIR, 'session.log');
|
|
10
|
+
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB rotation threshold
|
|
11
|
+
const SECRET_PATTERN = /(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL|BEARER|AUTHORIZATION)\s*[=:]\s*\S+/gi;
|
|
12
|
+
|
|
13
|
+
// MANDATORY stdin guard — exit silently if stdin unavailable
|
|
14
|
+
if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let input = '';
|
|
19
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 1500);
|
|
20
|
+
process.stdin.setEncoding('utf8');
|
|
21
|
+
process.stdin.on('error', () => process.exit(0));
|
|
22
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
23
|
+
process.stdin.on('end', () => {
|
|
24
|
+
clearTimeout(stdinTimeout);
|
|
25
|
+
try {
|
|
26
|
+
const data = JSON.parse(input);
|
|
27
|
+
|
|
28
|
+
// Safety check — matcher already filters, but guard explicitly
|
|
29
|
+
if (data.tool_name !== 'Bash') {
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cmd = ((data.tool_input && data.tool_input.command) || '').substring(0, 500);
|
|
34
|
+
const rawOut = ((data.tool_response && data.tool_response.output) || '').substring(0, 2000);
|
|
35
|
+
const out = rawOut.replace(SECRET_PATTERN, '[REDACTED]');
|
|
36
|
+
|
|
37
|
+
const timestamp = new Date().toISOString();
|
|
38
|
+
const entry = `[${timestamp}] CMD: ${cmd}\nOUT: ${out}\n---\n`;
|
|
39
|
+
|
|
40
|
+
// Ensure log directory exists (guard avoids redundant mkdirSync on hot path)
|
|
41
|
+
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
42
|
+
|
|
43
|
+
// Log rotation: truncate when exceeding threshold
|
|
44
|
+
try {
|
|
45
|
+
const stat = fs.statSync(LOG_FILE);
|
|
46
|
+
if (stat.size > MAX_LOG_SIZE) fs.writeFileSync(LOG_FILE, entry, { mode: 0o600 });
|
|
47
|
+
else fs.appendFileSync(LOG_FILE, entry, 'utf8');
|
|
48
|
+
} catch {
|
|
49
|
+
// File does not exist yet — create with restrictive permissions
|
|
50
|
+
fs.writeFileSync(LOG_FILE, entry, { mode: 0o600 });
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
|
|
54
|
+
// Always exit 0 — logging must never block execution
|
|
55
|
+
process.exit(0);
|
|
56
|
+
});
|
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Stop hook: Warn if TypeScript errors, missing CHANGELOG entry, or stale README
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { execSync } = require('child_process');
|
|
7
|
-
|
|
8
|
-
// MANDATORY stdin guard — exit silently if stdin unavailable (e.g. after /clear)
|
|
9
|
-
if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
|
|
10
|
-
process.exit(0);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let input = '';
|
|
14
|
-
const stdinTimeout = setTimeout(() => process.exit(0), 1500);
|
|
15
|
-
process.stdin.setEncoding('utf8');
|
|
16
|
-
process.stdin.on('error', () => process.exit(0));
|
|
17
|
-
process.stdin.on('data', chunk => { input += chunk; });
|
|
18
|
-
process.stdin.on('end', () => {
|
|
19
|
-
clearTimeout(stdinTimeout);
|
|
20
|
-
try {
|
|
21
|
-
const cwd = process.cwd();
|
|
22
|
-
|
|
23
|
-
// Kastell project guard
|
|
24
|
-
const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
|
|
25
|
-
fs.existsSync(path.join(cwd, 'package.json'));
|
|
26
|
-
if (!isKastell) {
|
|
27
|
-
try {
|
|
28
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
29
|
-
if (pkg.name !== 'kastell') process.exit(0);
|
|
30
|
-
} catch { process.exit(0); }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const warnings = [];
|
|
34
|
-
|
|
35
|
-
// Check 1: TypeScript build
|
|
36
|
-
try {
|
|
37
|
-
execSync('npx tsc --noEmit 2>&1', { cwd, timeout: 10000, stdio: 'pipe', windowsHide: true });
|
|
38
|
-
} catch {
|
|
39
|
-
warnings.push('TypeScript errors detected - run `npm run build` to see details');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Check 2: CHANGELOG version entry
|
|
43
|
-
try {
|
|
44
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
45
|
-
const changelog = fs.readFileSync(path.join(cwd, 'CHANGELOG.md'), 'utf8');
|
|
46
|
-
if (!changelog.includes(`## [${pkg.version}]`) && !changelog.includes(`## ${pkg.version}`)) {
|
|
47
|
-
warnings.push(`CHANGELOG.md missing entry for v${pkg.version}`);
|
|
48
|
-
}
|
|
49
|
-
} catch {}
|
|
50
|
-
|
|
51
|
-
// Check 3: README staleness (compare last commit timestamps)
|
|
52
|
-
try {
|
|
53
|
-
const srcFiles = execSync('git log -1 --format=%ct -- src/', {
|
|
54
|
-
cwd, encoding: 'utf8', timeout: 5000, windowsHide: true,
|
|
55
|
-
}).trim();
|
|
56
|
-
const readmeCommit = execSync('git log -1 --format=%ct -- README.md', {
|
|
57
|
-
cwd, encoding: 'utf8', timeout: 5000, windowsHide: true,
|
|
58
|
-
}).trim();
|
|
59
|
-
if (srcFiles && readmeCommit && parseInt(srcFiles) > parseInt(readmeCommit)) {
|
|
60
|
-
warnings.push('README.md may be stale - src/ has newer commits');
|
|
61
|
-
}
|
|
62
|
-
} catch {}
|
|
63
|
-
|
|
64
|
-
// Output warnings to stderr — Stop hooks CANNOT block execution
|
|
65
|
-
for (const w of warnings) {
|
|
66
|
-
process.stderr.write(`WARNING: ${w}\n`);
|
|
67
|
-
}
|
|
68
|
-
} catch {}
|
|
69
|
-
|
|
70
|
-
// Always exit 0 — Stop hooks must never fail session end
|
|
71
|
-
process.exit(0);
|
|
72
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Stop hook: Warn if TypeScript errors, missing CHANGELOG entry, or stale README
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
// MANDATORY stdin guard — exit silently if stdin unavailable (e.g. after /clear)
|
|
9
|
+
if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let input = '';
|
|
14
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 1500);
|
|
15
|
+
process.stdin.setEncoding('utf8');
|
|
16
|
+
process.stdin.on('error', () => process.exit(0));
|
|
17
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
18
|
+
process.stdin.on('end', () => {
|
|
19
|
+
clearTimeout(stdinTimeout);
|
|
20
|
+
try {
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
|
|
23
|
+
// Kastell project guard
|
|
24
|
+
const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
|
|
25
|
+
fs.existsSync(path.join(cwd, 'package.json'));
|
|
26
|
+
if (!isKastell) {
|
|
27
|
+
try {
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
29
|
+
if (pkg.name !== 'kastell') process.exit(0);
|
|
30
|
+
} catch { process.exit(0); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const warnings = [];
|
|
34
|
+
|
|
35
|
+
// Check 1: TypeScript build
|
|
36
|
+
try {
|
|
37
|
+
execSync('npx tsc --noEmit 2>&1', { cwd, timeout: 10000, stdio: 'pipe', windowsHide: true });
|
|
38
|
+
} catch {
|
|
39
|
+
warnings.push('TypeScript errors detected - run `npm run build` to see details');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check 2: CHANGELOG version entry
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
45
|
+
const changelog = fs.readFileSync(path.join(cwd, 'CHANGELOG.md'), 'utf8');
|
|
46
|
+
if (!changelog.includes(`## [${pkg.version}]`) && !changelog.includes(`## ${pkg.version}`)) {
|
|
47
|
+
warnings.push(`CHANGELOG.md missing entry for v${pkg.version}`);
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
|
|
51
|
+
// Check 3: README staleness (compare last commit timestamps)
|
|
52
|
+
try {
|
|
53
|
+
const srcFiles = execSync('git log -1 --format=%ct -- src/', {
|
|
54
|
+
cwd, encoding: 'utf8', timeout: 5000, windowsHide: true,
|
|
55
|
+
}).trim();
|
|
56
|
+
const readmeCommit = execSync('git log -1 --format=%ct -- README.md', {
|
|
57
|
+
cwd, encoding: 'utf8', timeout: 5000, windowsHide: true,
|
|
58
|
+
}).trim();
|
|
59
|
+
if (srcFiles && readmeCommit && parseInt(srcFiles) > parseInt(readmeCommit)) {
|
|
60
|
+
warnings.push('README.md may be stale - src/ has newer commits');
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
|
|
64
|
+
// Output warnings to stderr — Stop hooks CANNOT block execution
|
|
65
|
+
for (const w of warnings) {
|
|
66
|
+
process.stderr.write(`WARNING: ${w}\n`);
|
|
67
|
+
}
|
|
68
|
+
} catch {}
|
|
69
|
+
|
|
70
|
+
// Always exit 0 — Stop hooks must never fail session end
|
|
71
|
+
process.exit(0);
|
|
72
|
+
});
|