2ndbrain 2026.1.30 → 2026.1.32
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/settings.local.json +17 -0
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/db/migrations/001_initial_schema.sql +91 -0
- package/doc/SPEC.md +896 -0
- package/hooks/auto-capture.sh +4 -0
- package/hooks/validate-command.sh +374 -0
- package/package.json +34 -20
- package/skills/journal/SKILL.md +112 -0
- package/skills/knowledge/SKILL.md +165 -0
- package/skills/project-manage/SKILL.md +216 -0
- package/skills/recall/SKILL.md +182 -0
- package/skills/system-ops/SKILL.md +161 -0
- package/src/attachments/store.js +167 -0
- package/src/claude/bridge.js +291 -0
- package/src/claude/conversation.js +219 -0
- package/src/config.js +90 -0
- package/src/db/migrate.js +94 -0
- package/src/db/pool.js +33 -0
- package/src/embeddings/engine.js +281 -0
- package/src/embeddings/worker.js +221 -0
- package/src/hooks/lifecycle.js +448 -0
- package/src/index.js +560 -0
- package/src/logging.js +91 -0
- package/src/mcp/config.js +75 -0
- package/src/mcp/embed-server.js +242 -0
- package/src/rate-limiter.js +114 -0
- package/src/telegram/bot.js +546 -0
- package/src/telegram/commands.js +440 -0
- package/src/web/server.js +1119 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# validate-command.sh -- PreToolUse hook for Bash command whitelist enforcement
|
|
3
|
+
# Spec reference: section 13.2 Claude Subprocess Hooks - Command Whitelist Enforcement
|
|
4
|
+
#
|
|
5
|
+
# Invoked by claude-cli as a PreToolUse hook when the Bash tool is used.
|
|
6
|
+
# Reads JSON from stdin, extracts tool_input.command, and decides whether to
|
|
7
|
+
# allow or block the command.
|
|
8
|
+
#
|
|
9
|
+
# Exit codes:
|
|
10
|
+
# 0 = allow the command
|
|
11
|
+
# 2 = block the command (stdout contains JSON: { "reason": "..." })
|
|
12
|
+
#
|
|
13
|
+
# Environment variables:
|
|
14
|
+
# COMMANDS_WHITELIST - Comma-separated glob-style patterns of allowed commands
|
|
15
|
+
# FILE_EDIT_PATHS - Comma-separated absolute paths where file writes are allowed
|
|
16
|
+
# HOME - User home directory (writes always allowed here)
|
|
17
|
+
# DATABASE_URL - If set, blocked attempts are logged to system_logs via psql
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
###############################################################################
|
|
22
|
+
# Read JSON input from stdin
|
|
23
|
+
###############################################################################
|
|
24
|
+
INPUT=$(cat)
|
|
25
|
+
|
|
26
|
+
###############################################################################
|
|
27
|
+
# Extract the command string from tool_input.command
|
|
28
|
+
#
|
|
29
|
+
# Primary: use jq for reliable JSON parsing.
|
|
30
|
+
# Fallback: sed-based extraction when jq is not available.
|
|
31
|
+
###############################################################################
|
|
32
|
+
if command -v jq >/dev/null 2>&1; then
|
|
33
|
+
COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
34
|
+
else
|
|
35
|
+
# Fallback: sed-based extraction. Handles basic JSON escaping (\" and \\).
|
|
36
|
+
# Complex multi-line or deeply nested JSON values are not expected from
|
|
37
|
+
# claude-cli's Bash tool_input.
|
|
38
|
+
COMMAND=$(printf '%s' "$INPUT" \
|
|
39
|
+
| tr '\n' ' ' \
|
|
40
|
+
| sed 's/.*"command"[[:space:]]*:[[:space:]]*"//' \
|
|
41
|
+
| sed 's/"[[:space:]]*[,}].*//' \
|
|
42
|
+
| sed 's/\\"/"/g; s/\\\\/\\/g')
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if [ -z "$COMMAND" ]; then
|
|
46
|
+
printf '{"reason":"Could not extract command from hook input"}\n'
|
|
47
|
+
exit 2
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
###############################################################################
|
|
51
|
+
# Helper: log a blocked command to system_logs via psql
|
|
52
|
+
#
|
|
53
|
+
# Only runs when DATABASE_URL is set. Failures are silent -- logging must
|
|
54
|
+
# never prevent the block decision from being communicated to the caller.
|
|
55
|
+
###############################################################################
|
|
56
|
+
log_blocked() {
|
|
57
|
+
local reason="$1"
|
|
58
|
+
|
|
59
|
+
if [ -z "${DATABASE_URL:-}" ]; then
|
|
60
|
+
return
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Sanitize for SQL string literals (escape single quotes)
|
|
64
|
+
local safe_cmd
|
|
65
|
+
safe_cmd=$(printf '%s' "$COMMAND" | sed "s/'/''/g" | tr -d '\000-\037')
|
|
66
|
+
local safe_reason
|
|
67
|
+
safe_reason=$(printf '%s' "$reason" | sed "s/'/''/g" | tr -d '\000-\037')
|
|
68
|
+
|
|
69
|
+
psql "$DATABASE_URL" -q -c \
|
|
70
|
+
"INSERT INTO system_logs (level, source, content) VALUES ('warn', 'validate-command', 'BLOCKED: ${safe_reason} | command: ${safe_cmd}')" \
|
|
71
|
+
>/dev/null 2>&1 || true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
###############################################################################
|
|
75
|
+
# Helper: block a command with a reason
|
|
76
|
+
# - Outputs JSON reason to stdout (for claude-cli)
|
|
77
|
+
# - Outputs structured log entry to stderr (for parent process capture)
|
|
78
|
+
# - Logs to system_logs via psql if DATABASE_URL is set
|
|
79
|
+
###############################################################################
|
|
80
|
+
block() {
|
|
81
|
+
local reason="$1"
|
|
82
|
+
|
|
83
|
+
# Sanitize the command for safe JSON embedding (escape backslashes, quotes,
|
|
84
|
+
# and control characters to prevent injection into the JSON output).
|
|
85
|
+
local safe_cmd
|
|
86
|
+
safe_cmd=$(printf '%s' "$COMMAND" \
|
|
87
|
+
| sed 's/\\/\\\\/g; s/"/\\"/g' \
|
|
88
|
+
| tr -d '\000-\037')
|
|
89
|
+
local safe_reason
|
|
90
|
+
safe_reason=$(printf '%s' "$reason" \
|
|
91
|
+
| sed 's/\\/\\\\/g; s/"/\\"/g' \
|
|
92
|
+
| tr -d '\000-\037')
|
|
93
|
+
|
|
94
|
+
# stdout: JSON for claude-cli to read
|
|
95
|
+
printf '{"reason":"%s"}\n' "$safe_reason"
|
|
96
|
+
|
|
97
|
+
# stderr: structured log for the parent Node.js process
|
|
98
|
+
printf '{"event":"command_blocked","command":"%s","reason":"%s"}\n' \
|
|
99
|
+
"$safe_cmd" "$safe_reason" >&2
|
|
100
|
+
|
|
101
|
+
# Persist to database (fire-and-forget)
|
|
102
|
+
log_blocked "$reason" &
|
|
103
|
+
|
|
104
|
+
exit 2
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
###############################################################################
|
|
108
|
+
# Helper: check if command matches any COMMANDS_WHITELIST pattern
|
|
109
|
+
#
|
|
110
|
+
# Patterns are glob-style. A pattern matches if the full command string starts
|
|
111
|
+
# with the pattern (or the pattern equals the first word of the command).
|
|
112
|
+
###############################################################################
|
|
113
|
+
matches_whitelist() {
|
|
114
|
+
local cmd="$1"
|
|
115
|
+
|
|
116
|
+
if [ -z "${COMMANDS_WHITELIST:-}" ]; then
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Save and restore IFS
|
|
121
|
+
local old_ifs="$IFS"
|
|
122
|
+
IFS=','
|
|
123
|
+
# shellcheck disable=SC2086
|
|
124
|
+
set -- $COMMANDS_WHITELIST
|
|
125
|
+
IFS="$old_ifs"
|
|
126
|
+
|
|
127
|
+
for pattern in "$@"; do
|
|
128
|
+
# Trim leading/trailing whitespace
|
|
129
|
+
pattern=$(printf '%s' "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
130
|
+
[ -z "$pattern" ] && continue
|
|
131
|
+
|
|
132
|
+
# Glob-style match: pattern matches the whole command or its prefix
|
|
133
|
+
# shellcheck disable=SC2254
|
|
134
|
+
case "$cmd" in
|
|
135
|
+
$pattern) return 0 ;;
|
|
136
|
+
$pattern\ *) return 0 ;;
|
|
137
|
+
$pattern\;*) return 0 ;;
|
|
138
|
+
$pattern\|*) return 0 ;;
|
|
139
|
+
$pattern\&*) return 0 ;;
|
|
140
|
+
esac
|
|
141
|
+
done
|
|
142
|
+
|
|
143
|
+
return 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
###############################################################################
|
|
147
|
+
# Helper: check if an absolute path falls within a system directory
|
|
148
|
+
# /etc, /usr, /boot, /sys, /proc -- always blocked per spec section 14.5
|
|
149
|
+
###############################################################################
|
|
150
|
+
is_system_dir() {
|
|
151
|
+
local p="$1"
|
|
152
|
+
case "$p" in
|
|
153
|
+
/etc|/etc/*) return 0 ;;
|
|
154
|
+
/usr|/usr/*) return 0 ;;
|
|
155
|
+
/boot|/boot/*) return 0 ;;
|
|
156
|
+
/sys|/sys/*) return 0 ;;
|
|
157
|
+
/proc|/proc/*) return 0 ;;
|
|
158
|
+
*) return 1 ;;
|
|
159
|
+
esac
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
###############################################################################
|
|
163
|
+
# Helper: check if a path is within allowed write directories
|
|
164
|
+
# - Home directory (~, $HOME) is always allowed
|
|
165
|
+
# - Paths listed in FILE_EDIT_PATHS are allowed
|
|
166
|
+
# - System directories are never allowed (checked separately)
|
|
167
|
+
###############################################################################
|
|
168
|
+
path_is_allowed() {
|
|
169
|
+
local target_path="$1"
|
|
170
|
+
local home_dir="${HOME:-/home/$(whoami)}"
|
|
171
|
+
|
|
172
|
+
# Always allow writes within home directory
|
|
173
|
+
case "$target_path" in
|
|
174
|
+
"$home_dir"|"$home_dir"/*) return 0 ;;
|
|
175
|
+
"~"|"~/"*) return 0 ;;
|
|
176
|
+
"."*|[^/]*) return 0 ;; # Relative paths resolve under cwd (within home)
|
|
177
|
+
esac
|
|
178
|
+
|
|
179
|
+
# Check FILE_EDIT_PATHS
|
|
180
|
+
if [ -n "${FILE_EDIT_PATHS:-}" ]; then
|
|
181
|
+
local old_ifs="$IFS"
|
|
182
|
+
IFS=','
|
|
183
|
+
# shellcheck disable=SC2086
|
|
184
|
+
set -- $FILE_EDIT_PATHS
|
|
185
|
+
IFS="$old_ifs"
|
|
186
|
+
|
|
187
|
+
for allowed_path in "$@"; do
|
|
188
|
+
allowed_path=$(printf '%s' "$allowed_path" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
189
|
+
[ -z "$allowed_path" ] && continue
|
|
190
|
+
case "$target_path" in
|
|
191
|
+
"$allowed_path"|"$allowed_path"/*) return 0 ;;
|
|
192
|
+
esac
|
|
193
|
+
done
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
return 1
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
###############################################################################
|
|
200
|
+
# Helper: detect file-write targets in a command and validate paths
|
|
201
|
+
#
|
|
202
|
+
# Inspects output redirection (>, >>), and common write commands (cp, mv, tee,
|
|
203
|
+
# dd, rsync, mkdir, touch) for absolute-path targets. Blocks if the target is
|
|
204
|
+
# a system directory or outside allowed paths.
|
|
205
|
+
###############################################################################
|
|
206
|
+
check_write_targets() {
|
|
207
|
+
local cmd="$1"
|
|
208
|
+
|
|
209
|
+
# -- Output redirection targets (> /path, >> /path) --
|
|
210
|
+
local redirect_targets
|
|
211
|
+
redirect_targets=$(printf '%s' "$cmd" | grep -oE '>>?\s*/[^ ;|&)]+' | sed 's/>>*[[:space:]]*//' || true)
|
|
212
|
+
|
|
213
|
+
for target in $redirect_targets; do
|
|
214
|
+
[ -z "$target" ] && continue
|
|
215
|
+
case "$target" in
|
|
216
|
+
/dev/null|/dev/stdout|/dev/stderr) continue ;;
|
|
217
|
+
esac
|
|
218
|
+
if is_system_dir "$target"; then
|
|
219
|
+
block "Writing to system directory is not allowed: ${target}"
|
|
220
|
+
fi
|
|
221
|
+
if ! path_is_allowed "$target"; then
|
|
222
|
+
block "File write to ${target} is outside allowed paths (home directory and FILE_EDIT_PATHS)"
|
|
223
|
+
fi
|
|
224
|
+
done
|
|
225
|
+
|
|
226
|
+
# -- tee targets --
|
|
227
|
+
if printf '%s' "$cmd" | grep -qE '(^|[|;&])\s*tee\s'; then
|
|
228
|
+
local tee_targets
|
|
229
|
+
tee_targets=$(printf '%s' "$cmd" \
|
|
230
|
+
| grep -oE 'tee\s+(-[a-zA-Z]*\s+)*/?[^ ;|&)]+' \
|
|
231
|
+
| grep -oE '/[^ ;|&)]+$' || true)
|
|
232
|
+
for target in $tee_targets; do
|
|
233
|
+
[ -z "$target" ] && continue
|
|
234
|
+
if is_system_dir "$target"; then
|
|
235
|
+
block "Writing to system directory is not allowed: ${target}"
|
|
236
|
+
fi
|
|
237
|
+
if ! path_is_allowed "$target"; then
|
|
238
|
+
block "File write to ${target} is outside allowed paths (home directory and FILE_EDIT_PATHS)"
|
|
239
|
+
fi
|
|
240
|
+
done
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
# -- cp, mv, install, rsync destinations (last absolute-path argument) --
|
|
244
|
+
if printf '%s' "$cmd" | grep -qE '(^|[;&|])\s*(cp|mv|install|rsync)\s'; then
|
|
245
|
+
local abs_paths
|
|
246
|
+
abs_paths=$(printf '%s' "$cmd" | grep -oE '\s/[^ ;|&>)]+' | sed 's/^[[:space:]]*//' || true)
|
|
247
|
+
# The last absolute path is typically the destination
|
|
248
|
+
local last_path=""
|
|
249
|
+
for p in $abs_paths; do
|
|
250
|
+
last_path="$p"
|
|
251
|
+
done
|
|
252
|
+
if [ -n "$last_path" ]; then
|
|
253
|
+
if is_system_dir "$last_path"; then
|
|
254
|
+
block "Writing to system directory is not allowed: ${last_path}"
|
|
255
|
+
fi
|
|
256
|
+
if ! path_is_allowed "$last_path"; then
|
|
257
|
+
block "File write to ${last_path} is outside allowed paths (home directory and FILE_EDIT_PATHS)"
|
|
258
|
+
fi
|
|
259
|
+
fi
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
# -- mkdir, touch, chmod, chown, chgrp on system dirs --
|
|
263
|
+
if printf '%s' "$cmd" | grep -qE '(^|[;&|])\s*(mkdir|touch|chmod|chown|chgrp)\s'; then
|
|
264
|
+
local mod_paths
|
|
265
|
+
mod_paths=$(printf '%s' "$cmd" | grep -oE '\s/[^ ;|&>)]+' | sed 's/^[[:space:]]*//' || true)
|
|
266
|
+
for target in $mod_paths; do
|
|
267
|
+
[ -z "$target" ] && continue
|
|
268
|
+
if is_system_dir "$target"; then
|
|
269
|
+
block "Modifying system directory is not allowed: ${target}"
|
|
270
|
+
fi
|
|
271
|
+
done
|
|
272
|
+
fi
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
###############################################################################
|
|
276
|
+
# RULE 1: Explicit whitelist match -- allow immediately
|
|
277
|
+
###############################################################################
|
|
278
|
+
if matches_whitelist "$COMMAND"; then
|
|
279
|
+
exit 0
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
###############################################################################
|
|
283
|
+
# RULE 2: Block dangerous commands unconditionally
|
|
284
|
+
###############################################################################
|
|
285
|
+
|
|
286
|
+
# -- sudo (unless whitelisted above) --
|
|
287
|
+
if printf '%s' "$COMMAND" | grep -qE '(^|[;&|`(])\s*sudo\b'; then
|
|
288
|
+
block "sudo commands are not allowed"
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
# -- rm -rf (rm with both -r and -f flags in any order) --
|
|
292
|
+
if printf '%s' "$COMMAND" | grep -qE '(^|[;&|`(])\s*rm\s'; then
|
|
293
|
+
# Check for combined flags like -rf, -fr, -rfi, etc.
|
|
294
|
+
if printf '%s' "$COMMAND" | grep -qE '\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)\b'; then
|
|
295
|
+
block "rm -rf is not allowed"
|
|
296
|
+
fi
|
|
297
|
+
# Check for separate flags: rm -r -f, rm -f -r, rm --recursive --force, etc.
|
|
298
|
+
if printf '%s' "$COMMAND" | grep -qE '\brm\s.*\s-r\b.*\s-f\b|\brm\s.*\s-f\b.*\s-r\b'; then
|
|
299
|
+
block "rm -rf is not allowed"
|
|
300
|
+
fi
|
|
301
|
+
if printf '%s' "$COMMAND" | grep -qE '\brm\s.*--recursive.*--force|\brm\s.*--force.*--recursive'; then
|
|
302
|
+
block "rm -rf is not allowed"
|
|
303
|
+
fi
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
# -- shutdown, reboot, poweroff --
|
|
307
|
+
if printf '%s' "$COMMAND" | grep -qE '(^|[;&|`(])\s*(shutdown|reboot|poweroff)\b'; then
|
|
308
|
+
block "System shutdown/reboot/poweroff commands are not allowed"
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# -- kill, pkill, killall --
|
|
312
|
+
if printf '%s' "$COMMAND" | grep -qE '(^|[;&|`(])\s*(kill|pkill|killall)\b'; then
|
|
313
|
+
block "Process termination commands (kill/pkill/killall) are not allowed"
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
# -- Network configuration changes --
|
|
317
|
+
if printf '%s' "$COMMAND" | grep -qE '(^|[;&|`(])\s*(ifconfig|ip\s+(addr|link|route|rule)|iptables|ip6tables|nft\b|nftables|route\s|nmcli|netplan|iwconfig|wpa_supplicant)\b'; then
|
|
318
|
+
block "Network configuration changes are not allowed"
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
# -- Package installation / removal --
|
|
322
|
+
if printf '%s' "$COMMAND" | grep -qE '(^|[;&|`(])\s*(apt|apt-get|aptitude)\s+(install|remove|purge|upgrade|dist-upgrade|full-upgrade)\b'; then
|
|
323
|
+
block "Package management (apt) is not allowed"
|
|
324
|
+
fi
|
|
325
|
+
if printf '%s' "$COMMAND" | grep -qE '(^|[;&|`(])\s*(yum|dnf)\s+(install|remove|erase|upgrade|update)\b'; then
|
|
326
|
+
block "Package management (yum/dnf) is not allowed"
|
|
327
|
+
fi
|
|
328
|
+
if printf '%s' "$COMMAND" | grep -qE '(^|[;&|`(])\s*pip[23]?\s+install\b'; then
|
|
329
|
+
block "pip install is not allowed"
|
|
330
|
+
fi
|
|
331
|
+
if printf '%s' "$COMMAND" | grep -qE '(^|[;&|`(])\s*npm\s+install\s+(-g|--global)\b'; then
|
|
332
|
+
block "Global npm install is not allowed"
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
###############################################################################
|
|
336
|
+
# RULE 3: Check file-write targets against allowed paths
|
|
337
|
+
#
|
|
338
|
+
# Runs BEFORE the read-only allowance so that commands like
|
|
339
|
+
# "echo x > /etc/passwd" are caught by write-target inspection even though
|
|
340
|
+
# "echo" would otherwise be considered read-only.
|
|
341
|
+
#
|
|
342
|
+
# Detects output redirection and common write commands. Blocks writes to
|
|
343
|
+
# system directories unconditionally, and blocks writes outside HOME and
|
|
344
|
+
# FILE_EDIT_PATHS.
|
|
345
|
+
###############################################################################
|
|
346
|
+
check_write_targets "$COMMAND"
|
|
347
|
+
|
|
348
|
+
###############################################################################
|
|
349
|
+
# RULE 4: Allow read-only system queries
|
|
350
|
+
###############################################################################
|
|
351
|
+
READONLY_CMDS='uptime|free|df|du|ps|top|htop|who|whoami|id|hostname|uname'
|
|
352
|
+
READONLY_CMDS="${READONLY_CMDS}|date|cal|cat|less|more|head|tail|ls|ll|stat|file"
|
|
353
|
+
READONLY_CMDS="${READONLY_CMDS}|wc|grep|egrep|fgrep|rg|find|locate|which|whereis|type"
|
|
354
|
+
READONLY_CMDS="${READONLY_CMDS}|pg_isready|lsblk|lscpu|lsusb|lspci|lsof|mount"
|
|
355
|
+
READONLY_CMDS="${READONLY_CMDS}|env|printenv|echo|printf|test|true|false|pwd|realpath"
|
|
356
|
+
READONLY_CMDS="${READONLY_CMDS}|dig|nslookup|host|ping|traceroute|curl|wget|ss|netstat"
|
|
357
|
+
READONLY_CMDS="${READONLY_CMDS}|git\s+(status|log|diff|show|branch|tag|remote|rev-parse)"
|
|
358
|
+
READONLY_CMDS="${READONLY_CMDS}|node\s+--version|npm\s+(ls|list|view|info|outdated|search)"
|
|
359
|
+
READONLY_CMDS="${READONLY_CMDS}|claude\s+--version|systemctl\s+(status|is-active|is-enabled)"
|
|
360
|
+
READONLY_CMDS="${READONLY_CMDS}|journalctl|dmesg|timedatectl\s+status"
|
|
361
|
+
|
|
362
|
+
if printf '%s' "$COMMAND" | grep -qE "^\s*(${READONLY_CMDS})\b"; then
|
|
363
|
+
exit 0
|
|
364
|
+
fi
|
|
365
|
+
|
|
366
|
+
###############################################################################
|
|
367
|
+
# RULE 5: Default -- allow
|
|
368
|
+
#
|
|
369
|
+
# Commands not explicitly blocked by rules 2-4 and not matched by the
|
|
370
|
+
# whitelist (rule 1) are allowed. The layered checks above provide the
|
|
371
|
+
# primary security enforcement. Additional patterns can be added to the
|
|
372
|
+
# block list as needed.
|
|
373
|
+
###############################################################################
|
|
374
|
+
exit 0
|
package/package.json
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "2ndbrain",
|
|
3
|
-
"version": "2026.1.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
},
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "2ndbrain",
|
|
3
|
+
"version": "2026.1.32",
|
|
4
|
+
"description": "Always-on Node.js service bridging Telegram messaging to Claude AI with knowledge graph, journal, project management, and semantic search.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"2ndbrain": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"test": "node --test src/**/*.test.js"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/fingerskier/2ndbrain.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["telegram", "claude", "ai", "assistant", "knowledge-graph"],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/fingerskier/2ndbrain/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/fingerskier/2ndbrain#readme",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"dotenv": "^16.4.7",
|
|
27
|
+
"express": "^4.21.2",
|
|
28
|
+
"open": "^10.1.0",
|
|
29
|
+
"pg": "^8.13.1"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Skill: journal
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Create and search personal journal entries. Use this skill when the user wants to capture a thought, reflect on something, make a note to self, or recall past journal entries. Journal entries are free-form text stored with timestamps.
|
|
6
|
+
|
|
7
|
+
## When to Activate
|
|
8
|
+
|
|
9
|
+
Activate this skill when any of the following conditions are met:
|
|
10
|
+
|
|
11
|
+
- The user's message begins with `/journal`
|
|
12
|
+
- The user expresses intent to record a thought or note (e.g., "note to self", "I want to remember", "remind me that", "journal this", "write down that")
|
|
13
|
+
- The user asks to recall or search past journal entries (e.g., "what did I write about X", "my notes from last week", "find my journal entry about")
|
|
14
|
+
- The user is reflecting or wants to preserve a thought for later
|
|
15
|
+
|
|
16
|
+
## Available Tools
|
|
17
|
+
|
|
18
|
+
- `mcp__pg__query` -- Execute SQL queries against the PostgreSQL database.
|
|
19
|
+
|
|
20
|
+
No other tools are permitted for this skill.
|
|
21
|
+
|
|
22
|
+
## Database Tables
|
|
23
|
+
|
|
24
|
+
### `journal`
|
|
25
|
+
|
|
26
|
+
| Column | Type | Description |
|
|
27
|
+
|--------|------|-------------|
|
|
28
|
+
| `id` | SERIAL PRIMARY KEY | Auto-incrementing identifier |
|
|
29
|
+
| `created_at` | TIMESTAMPTZ | Timestamp of creation (defaults to NOW()) |
|
|
30
|
+
| `updated_at` | TIMESTAMPTZ | Timestamp of last update (defaults to NOW()) |
|
|
31
|
+
| `note` | TEXT NOT NULL | The journal entry content |
|
|
32
|
+
|
|
33
|
+
### `embeddings` (for post-create embedding queue only)
|
|
34
|
+
|
|
35
|
+
| Column | Type | Description |
|
|
36
|
+
|--------|------|-------------|
|
|
37
|
+
| `entity_type` | TEXT NOT NULL | Type of entity (use `'journal'` for journal entries) |
|
|
38
|
+
| `entity_id` | INTEGER NOT NULL | The `id` of the journal entry |
|
|
39
|
+
| `vector` | VECTOR | Embedding vector (managed by the embeddings engine) |
|
|
40
|
+
|
|
41
|
+
## Operations
|
|
42
|
+
|
|
43
|
+
### Create a Journal Entry
|
|
44
|
+
|
|
45
|
+
When the user wants to record something, insert a new row into the `journal` table.
|
|
46
|
+
|
|
47
|
+
```sql
|
|
48
|
+
INSERT INTO journal (note)
|
|
49
|
+
VALUES ('The user''s note content goes here')
|
|
50
|
+
RETURNING id, created_at;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
After creating the entry, always confirm to the user what was saved, including the timestamp.
|
|
54
|
+
|
|
55
|
+
### Search Journal Entries by Text
|
|
56
|
+
|
|
57
|
+
When the user asks to find or recall entries by keyword or topic, use case-insensitive pattern matching.
|
|
58
|
+
|
|
59
|
+
```sql
|
|
60
|
+
SELECT id, note, created_at
|
|
61
|
+
FROM journal
|
|
62
|
+
WHERE note ILIKE '%' || 'search term' || '%'
|
|
63
|
+
ORDER BY created_at DESC
|
|
64
|
+
LIMIT 10;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Date-Filtered Recall
|
|
68
|
+
|
|
69
|
+
When the user asks about entries from a specific time period (e.g., "last week", "in December", "yesterday"), filter by date range.
|
|
70
|
+
|
|
71
|
+
```sql
|
|
72
|
+
SELECT id, note, created_at
|
|
73
|
+
FROM journal
|
|
74
|
+
WHERE created_at >= '2026-01-24T00:00:00Z'
|
|
75
|
+
AND created_at < '2026-01-31T00:00:00Z'
|
|
76
|
+
ORDER BY created_at DESC;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Interpret relative time references (e.g., "last week", "yesterday", "past month") based on the current date provided in the system prompt.
|
|
80
|
+
|
|
81
|
+
### List Recent Entries
|
|
82
|
+
|
|
83
|
+
When the user asks to see recent journal entries without a specific search term.
|
|
84
|
+
|
|
85
|
+
```sql
|
|
86
|
+
SELECT id, note, created_at
|
|
87
|
+
FROM journal
|
|
88
|
+
ORDER BY created_at DESC
|
|
89
|
+
LIMIT 10;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Post-Create: Queue for Embedding
|
|
93
|
+
|
|
94
|
+
After successfully creating a journal entry, attempt to queue it for embedding generation. This enables future semantic search over journal content.
|
|
95
|
+
|
|
96
|
+
```sql
|
|
97
|
+
INSERT INTO embeddings (entity_type, entity_id)
|
|
98
|
+
VALUES ('journal', <new_entry_id>)
|
|
99
|
+
ON CONFLICT DO NOTHING;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Important:** The `embeddings` table only exists when the embeddings engine is enabled. If this query fails because the table does not exist, silently ignore the error and proceed normally. The journal entry is already saved -- embedding is optional and supplementary. Do not report the embedding queue failure to the user.
|
|
103
|
+
|
|
104
|
+
## Restrictions and Notes
|
|
105
|
+
|
|
106
|
+
- Always use `mcp__pg__query` for database operations. Do not use any other tool.
|
|
107
|
+
- Never delete or modify existing journal entries unless the user explicitly asks to edit or remove a specific entry.
|
|
108
|
+
- When presenting journal entries to the user, include the date/time of each entry for context.
|
|
109
|
+
- If the user's message is ambiguous about whether they want to create an entry or search for one, ask for clarification.
|
|
110
|
+
- Keep confirmations concise: after saving, say something like "Noted." or "Saved to your journal." along with the timestamp -- do not repeat the entire entry back unless it is very short.
|
|
111
|
+
- For search results, present entries in reverse chronological order with dates.
|
|
112
|
+
- If no results are found for a search, say so clearly and suggest broadening the search terms.
|