claude-recall 0.9.2 → 0.9.3
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/hooks/search_enforcer.py +148 -0
- package/dist/cli/claude-recall-cli.js +45 -40
- package/package.json +1 -1
- package/scripts/postinstall.js +45 -37
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Minimal Search Enforcer for Claude Recall v0.9.3+
|
|
4
|
+
|
|
5
|
+
Ensures memory search is performed before Write/Edit/Bash/Task operations.
|
|
6
|
+
Works alongside native Claude Skills - skill teaches, hook enforces.
|
|
7
|
+
|
|
8
|
+
State file: ~/.claude-recall/hook-state/{session_id}.json
|
|
9
|
+
Exit codes: 0 = allow, 2 = block
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
STATE_DIR = Path.home() / '.claude-recall' / 'hook-state'
|
|
18
|
+
SEARCH_TTL_MS = int(os.environ.get('CLAUDE_RECALL_SEARCH_TTL', 5 * 60 * 1000)) # 5 min default
|
|
19
|
+
ENFORCE_MODE = os.environ.get('CLAUDE_RECALL_ENFORCE_MODE', 'block') # block, warn, off
|
|
20
|
+
|
|
21
|
+
# Tools that count as "search performed"
|
|
22
|
+
SEARCH_TOOLS = [
|
|
23
|
+
'mcp__claude-recall__search',
|
|
24
|
+
'mcp__claude-recall__mcp__claude-recall__search',
|
|
25
|
+
'mcp__claude-recall__retrieve_memory',
|
|
26
|
+
'mcp__claude-recall__mcp__claude-recall__retrieve_memory',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Tools that require search first
|
|
30
|
+
ENFORCE_TOOLS = ['Write', 'Edit', 'Bash', 'Task']
|
|
31
|
+
|
|
32
|
+
# Read-only bash commands that don't need memory search
|
|
33
|
+
READ_ONLY_BASH = [
|
|
34
|
+
'ls', 'cat', 'head', 'tail', 'less', 'more', 'file', 'stat', 'wc',
|
|
35
|
+
'find', 'locate', 'which', 'whereis', 'type', 'pwd', 'whoami',
|
|
36
|
+
'git status', 'git log', 'git diff', 'git show', 'git branch',
|
|
37
|
+
'git remote', 'git fetch', 'git stash list', 'git tag',
|
|
38
|
+
'npm list', 'npm ls', 'npm view', 'npm outdated', 'npm audit',
|
|
39
|
+
'npm test', 'npm run test', 'npm run build', 'npm run lint',
|
|
40
|
+
'pip list', 'pip show', 'pip freeze',
|
|
41
|
+
'pytest', 'jest', 'cargo test', 'go test', 'tsc --noEmit',
|
|
42
|
+
'grep', 'rg', 'ag', 'awk', 'sed', 'sort', 'uniq', 'diff',
|
|
43
|
+
'tree', 'realpath', 'dirname', 'basename', 'date', 'env', 'echo',
|
|
44
|
+
'ps', 'top', 'df', 'du', 'free', 'uptime', 'hostname',
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_state_file(session_id: str) -> Path:
|
|
49
|
+
safe_id = "".join(c if c.isalnum() or c in '-_' else '_' for c in session_id) or 'default'
|
|
50
|
+
return STATE_DIR / f'{safe_id}.json'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_state(session_id: str) -> dict:
|
|
54
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
state_file = get_state_file(session_id)
|
|
56
|
+
if state_file.exists():
|
|
57
|
+
try:
|
|
58
|
+
return json.load(open(state_file))
|
|
59
|
+
except:
|
|
60
|
+
pass
|
|
61
|
+
return {'lastSearchAt': None, 'searchQuery': None}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def save_state(session_id: str, state: dict):
|
|
65
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
try:
|
|
67
|
+
json.dump(state, open(get_state_file(session_id), 'w'), indent=2)
|
|
68
|
+
except:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_search_tool(tool_name: str) -> bool:
|
|
73
|
+
return any(s in tool_name for s in SEARCH_TOOLS)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_read_only_bash(command: str) -> bool:
|
|
77
|
+
if not command:
|
|
78
|
+
return False
|
|
79
|
+
cmd = command.strip().lower()
|
|
80
|
+
# Check direct match or pipe starting with read-only
|
|
81
|
+
for ro in READ_ONLY_BASH:
|
|
82
|
+
if cmd.startswith(ro):
|
|
83
|
+
return True
|
|
84
|
+
if '|' in cmd:
|
|
85
|
+
first = cmd.split('|')[0].strip()
|
|
86
|
+
for ro in READ_ONLY_BASH:
|
|
87
|
+
if first.startswith(ro):
|
|
88
|
+
return True
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main():
|
|
93
|
+
if ENFORCE_MODE == 'off':
|
|
94
|
+
sys.exit(0)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
data = json.load(sys.stdin)
|
|
98
|
+
except:
|
|
99
|
+
sys.exit(0)
|
|
100
|
+
|
|
101
|
+
tool_name = data.get('tool_name', '')
|
|
102
|
+
tool_input = data.get('tool_input', {})
|
|
103
|
+
session_id = data.get('session_id', '') or 'default'
|
|
104
|
+
|
|
105
|
+
# Track search calls
|
|
106
|
+
if is_search_tool(tool_name):
|
|
107
|
+
state = load_state(session_id)
|
|
108
|
+
state['lastSearchAt'] = int(datetime.now().timestamp() * 1000)
|
|
109
|
+
state['searchQuery'] = tool_input.get('query', '')
|
|
110
|
+
save_state(session_id, state)
|
|
111
|
+
sys.exit(0)
|
|
112
|
+
|
|
113
|
+
# Only enforce on specific tools
|
|
114
|
+
if tool_name not in ENFORCE_TOOLS:
|
|
115
|
+
sys.exit(0)
|
|
116
|
+
|
|
117
|
+
# Skip read-only bash
|
|
118
|
+
if tool_name == 'Bash' and is_read_only_bash(tool_input.get('command', '')):
|
|
119
|
+
sys.exit(0)
|
|
120
|
+
|
|
121
|
+
# Check if search was recent
|
|
122
|
+
state = load_state(session_id)
|
|
123
|
+
last_search = state.get('lastSearchAt')
|
|
124
|
+
|
|
125
|
+
if last_search:
|
|
126
|
+
now = int(datetime.now().timestamp() * 1000)
|
|
127
|
+
if (now - last_search) <= SEARCH_TTL_MS:
|
|
128
|
+
sys.exit(0)
|
|
129
|
+
|
|
130
|
+
# Block or warn
|
|
131
|
+
msg = f"""
|
|
132
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
133
|
+
🔍 MEMORY SEARCH REQUIRED before {tool_name}
|
|
134
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
135
|
+
|
|
136
|
+
Run: mcp__claude-recall__search({{"query": "relevant keywords"}})
|
|
137
|
+
|
|
138
|
+
This ensures you apply user preferences and avoid past mistakes.
|
|
139
|
+
|
|
140
|
+
To disable: CLAUDE_RECALL_ENFORCE_MODE=off
|
|
141
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
142
|
+
"""
|
|
143
|
+
print(msg.strip(), file=sys.stderr)
|
|
144
|
+
sys.exit(0 if ENFORCE_MODE == 'warn' else 2)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == '__main__':
|
|
148
|
+
main()
|
|
@@ -565,23 +565,24 @@ async function main() {
|
|
|
565
565
|
}
|
|
566
566
|
}
|
|
567
567
|
}
|
|
568
|
-
// Install skills
|
|
569
|
-
function
|
|
568
|
+
// Install skills + minimal enforcement hook (v0.9.3+ hybrid approach)
|
|
569
|
+
function installSkillsAndHook(force = false) {
|
|
570
570
|
const cwd = process.cwd();
|
|
571
571
|
const projectName = path.basename(cwd);
|
|
572
|
-
console.log('\n📦 Claude Recall v0.9.
|
|
572
|
+
console.log('\n📦 Claude Recall v0.9.3+ Setup\n');
|
|
573
573
|
console.log(`📍 Project: ${projectName}`);
|
|
574
574
|
console.log(`📍 Directory: ${cwd}\n`);
|
|
575
575
|
// Find the package directory (where claude-recall is installed)
|
|
576
576
|
const packageDir = path.resolve(__dirname, '../..');
|
|
577
577
|
const packageSkillsDir = path.join(packageDir, '.claude/skills');
|
|
578
|
+
const packageHooksDir = path.join(packageDir, '.claude/hooks');
|
|
578
579
|
const claudeDir = path.join(cwd, '.claude');
|
|
579
580
|
const hooksDir = path.join(claudeDir, 'hooks');
|
|
580
581
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
581
|
-
// === CLEANUP: Remove
|
|
582
|
+
// === CLEANUP: Remove OLD hooks (not the new search_enforcer.py) ===
|
|
582
583
|
if (fs.existsSync(hooksDir)) {
|
|
583
584
|
const oldHooks = [
|
|
584
|
-
'memory_enforcer.py',
|
|
585
|
+
'memory_enforcer.py', // Old v0.8.x hook
|
|
585
586
|
'pre_tool_search_enforcer.py',
|
|
586
587
|
'mcp_tool_tracker.py',
|
|
587
588
|
'pubnub_pre_tool_hook.py',
|
|
@@ -597,49 +598,53 @@ async function main() {
|
|
|
597
598
|
removedCount++;
|
|
598
599
|
}
|
|
599
600
|
}
|
|
600
|
-
// Remove hooks directory if empty
|
|
601
|
-
try {
|
|
602
|
-
const remaining = fs.readdirSync(hooksDir);
|
|
603
|
-
if (remaining.length === 0) {
|
|
604
|
-
fs.rmdirSync(hooksDir);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
catch (e) {
|
|
608
|
-
// Ignore
|
|
609
|
-
}
|
|
610
601
|
if (removedCount > 0) {
|
|
611
|
-
console.log(`🧹 Removed ${removedCount} old hook file(s)
|
|
602
|
+
console.log(`🧹 Removed ${removedCount} old hook file(s)`);
|
|
612
603
|
}
|
|
613
604
|
}
|
|
614
|
-
// ===
|
|
605
|
+
// === INSTALL: New minimal search_enforcer.py ===
|
|
606
|
+
if (!fs.existsSync(hooksDir)) {
|
|
607
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
608
|
+
}
|
|
609
|
+
const hookSource = path.join(packageHooksDir, 'search_enforcer.py');
|
|
610
|
+
const hookDest = path.join(hooksDir, 'search_enforcer.py');
|
|
611
|
+
if (fs.existsSync(hookSource)) {
|
|
612
|
+
fs.copyFileSync(hookSource, hookDest);
|
|
613
|
+
fs.chmodSync(hookDest, 0o755);
|
|
614
|
+
console.log('✅ Installed search_enforcer.py to .claude/hooks/');
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
console.log(`⚠️ Hook not found at: ${hookSource}`);
|
|
618
|
+
}
|
|
619
|
+
// === CONFIGURE: Update settings.json with new hook ===
|
|
615
620
|
let settings = {};
|
|
616
621
|
if (fs.existsSync(settingsPath)) {
|
|
617
622
|
try {
|
|
618
623
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
619
|
-
const hadHooks = settings.hooks && Object.keys(settings.hooks).length > 0;
|
|
620
|
-
// Clear hooks - v0.9.0+ uses Skills instead
|
|
621
|
-
settings.hooks = {};
|
|
622
|
-
settings.hooksVersion = '2.0.0';
|
|
623
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
624
|
-
if (hadHooks) {
|
|
625
|
-
console.log('🧹 Cleared old hook configuration from .claude/settings.json');
|
|
626
|
-
}
|
|
627
624
|
}
|
|
628
625
|
catch (e) {
|
|
629
|
-
|
|
630
|
-
if (!fs.existsSync(claudeDir)) {
|
|
631
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
632
|
-
}
|
|
633
|
-
fs.writeFileSync(settingsPath, JSON.stringify({ hooks: {}, hooksVersion: '2.0.0' }, null, 2));
|
|
626
|
+
settings = {};
|
|
634
627
|
}
|
|
635
628
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
629
|
+
settings.hooksVersion = '3.0.0'; // v3 = hybrid (skill + minimal hook)
|
|
630
|
+
settings.hooks = {
|
|
631
|
+
PreToolUse: [
|
|
632
|
+
{
|
|
633
|
+
matcher: "mcp__claude-recall__.*|Write|Edit|Bash|Task",
|
|
634
|
+
hooks: [
|
|
635
|
+
{
|
|
636
|
+
type: "command",
|
|
637
|
+
command: `python3 ${hookDest}`
|
|
638
|
+
}
|
|
639
|
+
]
|
|
640
|
+
}
|
|
641
|
+
]
|
|
642
|
+
};
|
|
643
|
+
if (!fs.existsSync(claudeDir)) {
|
|
644
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
642
645
|
}
|
|
646
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
647
|
+
console.log('✅ Configured search enforcement hook');
|
|
643
648
|
// === INSTALL: Copy skills directory ===
|
|
644
649
|
if (fs.existsSync(packageSkillsDir)) {
|
|
645
650
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
@@ -650,7 +655,7 @@ async function main() {
|
|
|
650
655
|
console.log(`⚠️ Skills not found at: ${packageSkillsDir}`);
|
|
651
656
|
}
|
|
652
657
|
console.log('\n✅ Setup complete!\n');
|
|
653
|
-
console.log('ℹ️ v0.9.
|
|
658
|
+
console.log('ℹ️ v0.9.3+ uses Skills (guidance) + minimal hook (enforcement).');
|
|
654
659
|
console.log('Restart Claude Code to activate.\n');
|
|
655
660
|
}
|
|
656
661
|
// Setup command - shows activation instructions or installs skills
|
|
@@ -660,8 +665,8 @@ async function main() {
|
|
|
660
665
|
.option('--install', 'Install skills and clean up old hooks')
|
|
661
666
|
.action((options) => {
|
|
662
667
|
if (options.install) {
|
|
663
|
-
// Install skills and
|
|
664
|
-
|
|
668
|
+
// Install skills and enforcement hook
|
|
669
|
+
installSkillsAndHook();
|
|
665
670
|
}
|
|
666
671
|
else {
|
|
667
672
|
// Show activation instructions
|
|
@@ -692,7 +697,7 @@ async function main() {
|
|
|
692
697
|
.description('Clean up old hooks and install skills (v0.9.0+ migration)')
|
|
693
698
|
.option('--force', 'Force overwrite existing configuration')
|
|
694
699
|
.action((options) => {
|
|
695
|
-
|
|
700
|
+
installSkillsAndHook(options.force || false);
|
|
696
701
|
process.exit(0);
|
|
697
702
|
});
|
|
698
703
|
// Check hooks function
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-recall",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Persistent memory for Claude Code with native Skills integration, automatic capture, failure learning, and project scoping via MCP server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
package/scripts/postinstall.js
CHANGED
|
@@ -104,22 +104,22 @@ try {
|
|
|
104
104
|
console.log('⚠️ Failed to register project (non-fatal):', error.message);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// Install skills
|
|
107
|
+
// Install skills + minimal enforcement hook (v0.9.3+ hybrid approach)
|
|
108
108
|
try {
|
|
109
109
|
const cwd = process.cwd();
|
|
110
110
|
const projectName = path.basename(cwd);
|
|
111
111
|
const packageSkillsDir = path.join(__dirname, '../.claude/skills');
|
|
112
|
+
const packageHooksDir = path.join(__dirname, '../.claude/hooks');
|
|
112
113
|
|
|
113
114
|
if (projectName !== 'claude-recall' && !cwd.includes('node_modules/.pnpm') && !cwd.includes('node_modules/claude-recall')) {
|
|
114
115
|
const claudeDir = path.join(cwd, '.claude');
|
|
115
116
|
const hooksDir = path.join(claudeDir, 'hooks');
|
|
116
117
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
117
118
|
|
|
118
|
-
// === CLEANUP: Remove
|
|
119
|
+
// === CLEANUP: Remove OLD hooks (not the new search_enforcer.py) ===
|
|
119
120
|
if (fs.existsSync(hooksDir)) {
|
|
120
|
-
// Remove known hook files from previous versions
|
|
121
121
|
const oldHooks = [
|
|
122
|
-
'memory_enforcer.py',
|
|
122
|
+
'memory_enforcer.py', // Old v0.8.x hook
|
|
123
123
|
'pre_tool_search_enforcer.py',
|
|
124
124
|
'mcp_tool_tracker.py',
|
|
125
125
|
'pubnub_pre_tool_hook.py',
|
|
@@ -137,48 +137,56 @@ try {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
// Remove hooks directory if empty
|
|
141
|
-
try {
|
|
142
|
-
const remaining = fs.readdirSync(hooksDir);
|
|
143
|
-
if (remaining.length === 0) {
|
|
144
|
-
fs.rmdirSync(hooksDir);
|
|
145
|
-
}
|
|
146
|
-
} catch (e) {
|
|
147
|
-
// Ignore
|
|
148
|
-
}
|
|
149
|
-
|
|
150
140
|
if (removedCount > 0) {
|
|
151
|
-
console.log(`🧹 Removed ${removedCount} old hook file(s)
|
|
141
|
+
console.log(`🧹 Removed ${removedCount} old hook file(s)`);
|
|
152
142
|
}
|
|
153
143
|
}
|
|
154
144
|
|
|
155
|
-
// ===
|
|
156
|
-
if (fs.existsSync(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const hadHooks = settings.hooks && Object.keys(settings.hooks).length > 0;
|
|
145
|
+
// === INSTALL: New minimal search_enforcer.py ===
|
|
146
|
+
if (!fs.existsSync(hooksDir)) {
|
|
147
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
148
|
+
}
|
|
160
149
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
settings.hooksVersion = '2.0.0'; // Bump version to indicate skills-based
|
|
150
|
+
const hookSource = path.join(packageHooksDir, 'search_enforcer.py');
|
|
151
|
+
const hookDest = path.join(hooksDir, 'search_enforcer.py');
|
|
164
152
|
|
|
165
|
-
|
|
153
|
+
if (fs.existsSync(hookSource)) {
|
|
154
|
+
fs.copyFileSync(hookSource, hookDest);
|
|
155
|
+
fs.chmodSync(hookDest, 0o755);
|
|
156
|
+
console.log('✅ Installed search_enforcer.py to .claude/hooks/');
|
|
157
|
+
}
|
|
166
158
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
159
|
+
// === CONFIGURE: Update settings.json with new hook ===
|
|
160
|
+
let settings = {};
|
|
161
|
+
if (fs.existsSync(settingsPath)) {
|
|
162
|
+
try {
|
|
163
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
170
164
|
} catch (e) {
|
|
171
|
-
|
|
172
|
-
fs.writeFileSync(settingsPath, JSON.stringify({ hooks: {}, hooksVersion: '2.0.0' }, null, 2));
|
|
165
|
+
settings = {};
|
|
173
166
|
}
|
|
174
|
-
} else {
|
|
175
|
-
// Create new settings.json without hooks
|
|
176
|
-
if (!fs.existsSync(claudeDir)) {
|
|
177
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
178
|
-
}
|
|
179
|
-
fs.writeFileSync(settingsPath, JSON.stringify({ hooks: {}, hooksVersion: '2.0.0' }, null, 2));
|
|
180
167
|
}
|
|
181
168
|
|
|
169
|
+
settings.hooksVersion = '3.0.0'; // v3 = hybrid (skill + minimal hook)
|
|
170
|
+
settings.hooks = {
|
|
171
|
+
PreToolUse: [
|
|
172
|
+
{
|
|
173
|
+
matcher: "mcp__claude-recall__.*|Write|Edit|Bash|Task",
|
|
174
|
+
hooks: [
|
|
175
|
+
{
|
|
176
|
+
type: "command",
|
|
177
|
+
command: `python3 ${hookDest}`
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (!fs.existsSync(claudeDir)) {
|
|
185
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
186
|
+
}
|
|
187
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
188
|
+
console.log('✅ Configured search enforcement hook');
|
|
189
|
+
|
|
182
190
|
// === INSTALL: Copy skills directory ===
|
|
183
191
|
if (fs.existsSync(packageSkillsDir)) {
|
|
184
192
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
@@ -187,7 +195,7 @@ try {
|
|
|
187
195
|
}
|
|
188
196
|
}
|
|
189
197
|
} catch (error) {
|
|
190
|
-
console.log('⚠️ Failed to install
|
|
198
|
+
console.log('⚠️ Failed to install (non-fatal):', error.message);
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
console.log('\n✅ Installation complete!\n');
|
|
@@ -201,7 +209,7 @@ try {
|
|
|
201
209
|
console.log('');
|
|
202
210
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
203
211
|
console.log('');
|
|
204
|
-
console.log('ℹ️ v0.9.
|
|
212
|
+
console.log('ℹ️ v0.9.3+ uses Skills (guidance) + minimal hook (enforcement).');
|
|
205
213
|
console.log('💡 Your memories persist across conversations and restarts.\n');
|
|
206
214
|
|
|
207
215
|
} catch (error) {
|