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.
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ # auto-capture.sh -- PostToolUse hook for automatic knowledge capture
3
+ # Deferred to v2. See spec §13.2.
4
+ exit 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.30",
4
- "description": "An always-on Node.js npx service that bridges Telegram messges to Claude with\r * persistent conversation history (logs)\r * receive text messages w/ attachments\r * slash commands\r * send text message responses w/ \"Typing\" indicator\r * whitelist users that it will interact with (multi-layered)\r * can run local commands, access local postgres (mcp) (whitelisted)",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/fingerskier/2ndbrain.git"
12
- },
13
- "keywords": [],
14
- "author": "",
15
- "license": "ISC",
16
- "bugs": {
17
- "url": "https://github.com/fingerskier/2ndbrain/issues"
18
- },
19
- "homepage": "https://github.com/fingerskier/2ndbrain#readme"
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.