claude-smith 3.0.0 → 3.2.0
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/README.ko.md +45 -3
- package/README.md +45 -3
- package/bin/cli.mjs +331 -39
- package/hooks/batch-checkpoint.mjs +9 -22
- package/hooks/build-guard.mjs +7 -12
- package/hooks/build-tracker.mjs +9 -13
- package/hooks/commit-guard.mjs +20 -16
- package/hooks/commit-message.mjs +17 -17
- package/hooks/debug-loop.mjs +11 -22
- package/hooks/file-size-warn.mjs +7 -12
- package/hooks/plan-guard.mjs +14 -24
- package/hooks/scope-guard.mjs +9 -23
- package/hooks/subagent-inject.mjs +6 -12
- package/hooks/tdd-guard.mjs +8 -13
- package/hooks/test-tracker.mjs +10 -13
- package/lib/config.mjs +4 -1
- package/lib/event-log.mjs +96 -0
- package/lib/hook-utils.mjs +69 -0
- package/lib/stats.mjs +32 -5
- package/package.json +1 -1
- package/templates/commands/smith-dashboard.md +10 -0
- package/templates/commands/smith-update.md +1 -1
- package/templates/rules.en.md +3 -3
- package/templates/rules.ja.md +3 -3
- package/templates/rules.ko.md +3 -3
- package/templates/rules.md +3 -3
- package/templates/rules.zh.md +3 -3
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Event Log Library
|
|
5
|
+
* Appends structured events to .smith/events/YYYY-MM-DD.jsonl
|
|
6
|
+
* Provides project-level audit trail of hook activity
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { appendFileSync, readFileSync, mkdirSync, existsSync, readdirSync, lstatSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Append an event to the project-level event log.
|
|
14
|
+
* Writes to .smith/events/YYYY-MM-DD.jsonl
|
|
15
|
+
*
|
|
16
|
+
* @param {string} projectDir - project root (process.cwd())
|
|
17
|
+
* @param {object} event - { hook, event, file?, message? }
|
|
18
|
+
*/
|
|
19
|
+
export function appendEvent(projectDir, { hook, event, file = '', message = '' }) {
|
|
20
|
+
try {
|
|
21
|
+
const eventsDir = join(projectDir, '.smith', 'events');
|
|
22
|
+
mkdirSync(eventsDir, { recursive: true, mode: 0o700 });
|
|
23
|
+
|
|
24
|
+
// Symlink check: ensure events dir is a real directory, not a symlink
|
|
25
|
+
const stat = lstatSync(eventsDir);
|
|
26
|
+
if (!stat.isDirectory()) return; // Don't write to symlinks
|
|
27
|
+
|
|
28
|
+
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
29
|
+
const logFile = join(eventsDir, `${today}.jsonl`);
|
|
30
|
+
|
|
31
|
+
// Also check the log file isn't a symlink
|
|
32
|
+
if (existsSync(logFile)) {
|
|
33
|
+
const fileStat = lstatSync(logFile);
|
|
34
|
+
if (!fileStat.isFile()) return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const entry = JSON.stringify({
|
|
38
|
+
t: new Date().toISOString(),
|
|
39
|
+
h: hook,
|
|
40
|
+
e: event,
|
|
41
|
+
f: file,
|
|
42
|
+
m: message
|
|
43
|
+
}) + '\n';
|
|
44
|
+
|
|
45
|
+
appendFileSync(logFile, entry, { mode: 0o600 });
|
|
46
|
+
} catch {
|
|
47
|
+
// Silent fail — event logging should never break hooks
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read events from the last N days
|
|
53
|
+
*
|
|
54
|
+
* @param {string} projectDir - project root
|
|
55
|
+
* @param {object} options - { days: 7 }
|
|
56
|
+
* @returns {Array} events
|
|
57
|
+
*/
|
|
58
|
+
export function readEvents(projectDir, { days = 7 } = {}) {
|
|
59
|
+
try {
|
|
60
|
+
const eventsDir = join(projectDir, '.smith', 'events');
|
|
61
|
+
if (!existsSync(eventsDir)) return [];
|
|
62
|
+
|
|
63
|
+
// Symlink check: ensure events dir is a real directory
|
|
64
|
+
const stat = lstatSync(eventsDir);
|
|
65
|
+
if (!stat.isDirectory()) return [];
|
|
66
|
+
|
|
67
|
+
// Calculate cutoff date for filtering
|
|
68
|
+
const cutoff = new Date();
|
|
69
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
70
|
+
const cutoffStr = cutoff.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
71
|
+
|
|
72
|
+
const files = readdirSync(eventsDir)
|
|
73
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
74
|
+
.filter(f => {
|
|
75
|
+
const dateStr = f.replace('.jsonl', '');
|
|
76
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(dateStr) && dateStr >= cutoffStr;
|
|
77
|
+
})
|
|
78
|
+
.sort();
|
|
79
|
+
|
|
80
|
+
const events = [];
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
const content = readFileSync(join(eventsDir, file), 'utf8');
|
|
83
|
+
for (const line of content.split('\n')) {
|
|
84
|
+
if (!line.trim()) continue;
|
|
85
|
+
try {
|
|
86
|
+
events.push(JSON.parse(line));
|
|
87
|
+
} catch {
|
|
88
|
+
// Skip malformed lines
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return events;
|
|
93
|
+
} catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Utilities
|
|
3
|
+
* Common functions used across hook scripts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sanitize session/agent IDs to prevent path traversal
|
|
11
|
+
* Currently duplicated 11 times across hooks
|
|
12
|
+
* @param {string} id - The ID to sanitize
|
|
13
|
+
* @returns {string} Sanitized ID safe for filesystem use
|
|
14
|
+
*/
|
|
15
|
+
export function sanitizeId(id) {
|
|
16
|
+
if (!id || typeof id !== 'string') return 'default';
|
|
17
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if OMC autonomous mode is active
|
|
22
|
+
* Currently duplicated 4 times (debug-loop, scope-guard, batch-checkpoint, plan-guard)
|
|
23
|
+
* @param {string} cwd - Current working directory
|
|
24
|
+
* @returns {boolean} True if any OMC autonomous mode is active
|
|
25
|
+
*/
|
|
26
|
+
export function isOmcAutoMode(cwd) {
|
|
27
|
+
const omcStateDir = join(cwd, '.omc', 'state');
|
|
28
|
+
const omcAutoModes = ['autopilot-state.json', 'ultrawork-state.json', 'ralph-state.json'];
|
|
29
|
+
return omcAutoModes.some(f => {
|
|
30
|
+
try {
|
|
31
|
+
const state = JSON.parse(readFileSync(join(omcStateDir, f), 'utf8'));
|
|
32
|
+
return state.active === true || state.status === 'running';
|
|
33
|
+
} catch { return false; }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse hook input from stdin (JSON)
|
|
39
|
+
* Common pattern across all hooks
|
|
40
|
+
* @returns {object|null} Parsed input or null if parsing fails
|
|
41
|
+
*/
|
|
42
|
+
export function parseHookInput() {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(readFileSync(0, 'utf8'));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Send a brief notification to stderr for user visibility
|
|
52
|
+
* Only active when notify: true in .claude-smith.json
|
|
53
|
+
* @param {boolean} notify - Whether notifications are enabled
|
|
54
|
+
* @param {string} hookName - Display name of the hook
|
|
55
|
+
* @param {string} eventType - fire|warn|block|inject|track
|
|
56
|
+
* @param {string} message - Brief description
|
|
57
|
+
*/
|
|
58
|
+
export function notifyUser(notify, hookName, eventType, message) {
|
|
59
|
+
if (!notify) return;
|
|
60
|
+
const icons = {
|
|
61
|
+
fire: '✅',
|
|
62
|
+
warn: '⚠️',
|
|
63
|
+
block: '🚫',
|
|
64
|
+
inject: '🤖',
|
|
65
|
+
track: '📊'
|
|
66
|
+
};
|
|
67
|
+
const icon = icons[eventType] || '📝';
|
|
68
|
+
process.stderr.write(`🕵️ Smith — ${icon} ${hookName}: ${message}\n`);
|
|
69
|
+
}
|
package/lib/stats.mjs
CHANGED
|
@@ -7,8 +7,20 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { tmpdir } from 'os';
|
|
9
9
|
|
|
10
|
+
const KNOWN_EVENTS = ['fire', 'warn', 'block'];
|
|
11
|
+
|
|
10
12
|
export function recordEvent(sessionId, hookName, eventType) {
|
|
11
|
-
|
|
13
|
+
// Guard: only record known event types
|
|
14
|
+
if (!KNOWN_EVENTS.includes(eventType)) return;
|
|
15
|
+
|
|
16
|
+
// Sanitize sessionId and validate path
|
|
17
|
+
const safeId = (sessionId || 'default').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
18
|
+
const stateDir = join(tmpdir(), '.claude-smith', safeId);
|
|
19
|
+
|
|
20
|
+
// Verify resolved path is under expected parent
|
|
21
|
+
const expectedParent = join(tmpdir(), '.claude-smith');
|
|
22
|
+
if (!stateDir.startsWith(expectedParent)) return;
|
|
23
|
+
|
|
12
24
|
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
13
25
|
|
|
14
26
|
const counterFile = join(stateDir, `stat-${hookName}-${eventType}`);
|
|
@@ -18,16 +30,31 @@ export function recordEvent(sessionId, hookName, eventType) {
|
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
export function readStats(sessionId) {
|
|
21
|
-
|
|
33
|
+
// Sanitize sessionId and validate path
|
|
34
|
+
const safeId = (sessionId || 'default').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
35
|
+
const stateDir = join(tmpdir(), '.claude-smith', safeId);
|
|
36
|
+
|
|
37
|
+
// Verify resolved path is under expected parent
|
|
38
|
+
const expectedParent = join(tmpdir(), '.claude-smith');
|
|
39
|
+
if (!stateDir.startsWith(expectedParent)) return {};
|
|
40
|
+
|
|
22
41
|
const stats = {};
|
|
23
42
|
|
|
24
43
|
try {
|
|
25
44
|
const files = readdirSync(stateDir).filter(f => f.startsWith('stat-'));
|
|
26
45
|
for (const f of files) {
|
|
27
46
|
// Format: stat-{hookName}-{eventType}
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
47
|
+
// Use safer parsing with explicit known event types
|
|
48
|
+
const raw = f.replace('stat-', ''); // e.g., "batch-checkpoint-warn"
|
|
49
|
+
const lastDash = raw.lastIndexOf('-');
|
|
50
|
+
if (lastDash === -1) continue;
|
|
51
|
+
|
|
52
|
+
const eventType = raw.slice(lastDash + 1);
|
|
53
|
+
const hookName = raw.slice(0, lastDash);
|
|
54
|
+
|
|
55
|
+
// Skip unknown event types
|
|
56
|
+
if (!KNOWN_EVENTS.includes(eventType)) continue;
|
|
57
|
+
|
|
31
58
|
if (!stats[hookName]) stats[hookName] = { fire: 0, warn: 0, block: 0 };
|
|
32
59
|
try {
|
|
33
60
|
stats[hookName][eventType] = parseInt(readFileSync(join(stateDir, f), 'utf8'), 10) || 0;
|
package/package.json
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Generate the visual HTML compliance dashboard and open it in the browser.
|
|
2
|
+
|
|
3
|
+
Run: `npx claude-smith dashboard`
|
|
4
|
+
|
|
5
|
+
Then open the generated file:
|
|
6
|
+
- macOS: `open claude-smith-dashboard.html`
|
|
7
|
+
- Linux: `xdg-open claude-smith-dashboard.html`
|
|
8
|
+
- Windows: `start claude-smith-dashboard.html`
|
|
9
|
+
|
|
10
|
+
Display a brief summary of the dashboard contents to the user.
|
|
@@ -4,6 +4,6 @@ Steps:
|
|
|
4
4
|
1. Run: `npx claude-smith --version` to get the current installed version
|
|
5
5
|
2. Run: `npm view claude-smith version 2>/dev/null` to check the latest published version
|
|
6
6
|
3. Compare the two versions:
|
|
7
|
-
- If they match: Report "
|
|
7
|
+
- If they match: Report "🕵️ Smith is up to date (vX.X.X)"
|
|
8
8
|
- If latest is newer: Report the version difference and run `npx claude-smith update` to apply the update
|
|
9
9
|
- If npm check fails: Report that version check failed (possibly offline) and suggest running `npx claude-smith update` manually
|
package/templates/rules.en.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<!-- CLAUDE-SMITH:START
|
|
2
|
-
#
|
|
1
|
+
<!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
|
|
2
|
+
# 🕵️ Smith - Workflow Enforcement Rules
|
|
3
3
|
|
|
4
|
-
> Installed by
|
|
4
|
+
> Installed by 🕵️ Smith {{SMITH_VERSION}}. Do not edit manually.
|
|
5
5
|
> Update with: `claude-smith update`
|
|
6
6
|
|
|
7
7
|
## 1. Workflow Protocol (auto-applied)
|
package/templates/rules.ja.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<!-- CLAUDE-SMITH:START
|
|
2
|
-
#
|
|
1
|
+
<!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
|
|
2
|
+
# 🕵️ Smith - ワークフロー実行ルール
|
|
3
3
|
|
|
4
|
-
>
|
|
4
|
+
> 🕵️ Smith {{SMITH_VERSION}} によりインストール。手動で編集しないでください。
|
|
5
5
|
> 更新コマンド:`claude-smith update`
|
|
6
6
|
|
|
7
7
|
## 1. ワークフロープロトコル(自動適用)
|
package/templates/rules.ko.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<!-- CLAUDE-SMITH:START
|
|
2
|
-
#
|
|
1
|
+
<!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
|
|
2
|
+
# 🕵️ Smith - Workflow Enforcement Rules
|
|
3
3
|
|
|
4
|
-
> Installed by
|
|
4
|
+
> Installed by 🕵️ Smith {{SMITH_VERSION}}. Do not edit manually.
|
|
5
5
|
> Update with: `claude-smith update`
|
|
6
6
|
|
|
7
7
|
## 1. Workflow Protocol (자동 적용)
|
package/templates/rules.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<!-- CLAUDE-SMITH:START
|
|
2
|
-
#
|
|
1
|
+
<!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
|
|
2
|
+
# 🕵️ Smith - Workflow Enforcement Rules
|
|
3
3
|
|
|
4
|
-
> Installed by
|
|
4
|
+
> Installed by 🕵️ Smith {{SMITH_VERSION}}. Do not edit manually.
|
|
5
5
|
> Update with: `claude-smith update`
|
|
6
6
|
|
|
7
7
|
## 1. Workflow Protocol (auto-applied)
|
package/templates/rules.zh.md
CHANGED