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.
@@ -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
- const stateDir = join(tmpdir(), '.claude-smith', sessionId || 'default');
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
- const stateDir = join(tmpdir(), '.claude-smith', sessionId || 'default');
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
- const parts = f.replace('stat-', '').split('-');
29
- const eventType = parts.pop();
30
- const hookName = parts.join('-');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smith",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "Claude Code workflow enforcement CLI - forging coding discipline into every session",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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 "🔨 Smith is up to date (vX.X.X)"
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
@@ -1,7 +1,7 @@
1
- <!-- CLAUDE-SMITH:START v1.0.0 -->
2
- # 🔨 Smith - Workflow Enforcement Rules
1
+ <!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
2
+ # 🕵️ Smith - Workflow Enforcement Rules
3
3
 
4
- > Installed by 🔨 Smith v1.0.0. Do not edit manually.
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)
@@ -1,7 +1,7 @@
1
- <!-- CLAUDE-SMITH:START v1.0.0 -->
2
- # 🔨 Smith - ワークフロー実行ルール
1
+ <!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
2
+ # 🕵️ Smith - ワークフロー実行ルール
3
3
 
4
- > 🔨 Smith v1.0.0 によりインストール。手動で編集しないでください。
4
+ > 🕵️ Smith {{SMITH_VERSION}} によりインストール。手動で編集しないでください。
5
5
  > 更新コマンド:`claude-smith update`
6
6
 
7
7
  ## 1. ワークフロープロトコル(自動適用)
@@ -1,7 +1,7 @@
1
- <!-- CLAUDE-SMITH:START v1.0.0 -->
2
- # 🔨 Smith - Workflow Enforcement Rules
1
+ <!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
2
+ # 🕵️ Smith - Workflow Enforcement Rules
3
3
 
4
- > Installed by 🔨 Smith v1.0.0. Do not edit manually.
4
+ > Installed by 🕵️ Smith {{SMITH_VERSION}}. Do not edit manually.
5
5
  > Update with: `claude-smith update`
6
6
 
7
7
  ## 1. Workflow Protocol (자동 적용)
@@ -1,7 +1,7 @@
1
- <!-- CLAUDE-SMITH:START v1.0.0 -->
2
- # 🔨 Smith - Workflow Enforcement Rules
1
+ <!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
2
+ # 🕵️ Smith - Workflow Enforcement Rules
3
3
 
4
- > Installed by 🔨 Smith v1.0.0. Do not edit manually.
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)
@@ -1,7 +1,7 @@
1
- <!-- CLAUDE-SMITH:START v1.0.0 -->
2
- # 🔨 Smith - 工作流执行规则
1
+ <!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
2
+ # 🕵️ Smith - 工作流执行规则
3
3
 
4
- > 由 🔨 Smith v1.0.0 安装。请勿手动编辑。
4
+ > 由 🕵️ Smith {{SMITH_VERSION}} 安装。请勿手动编辑。
5
5
  > 更新命令:`claude-smith update`
6
6
 
7
7
  ## 1. 工作流协议(自动应用)