@remnic/plugin-claude-code 9.3.572 → 9.3.573

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "remnic",
3
3
  "description": "Universal memory for AI agents — automatic recall, observation, and cross-agent knowledge sharing",
4
- "version": "9.3.572",
4
+ "version": "9.3.573",
5
5
  "author": "Joshua Warren",
6
6
  "homepage": "https://github.com/joshuaswarren/remnic",
7
7
  "repository": "https://github.com/joshuaswarren/remnic"
@@ -61,31 +61,147 @@ CWD="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.cwd|
61
61
  TOOL_NAME="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.tool_name||'')" "$INPUT" 2>/dev/null || echo "")"
62
62
  PROJECT_NAME="$(basename "$CWD" 2>/dev/null || echo "unknown")"
63
63
 
64
- [ -z "$SESSION_ID" ] && exit 0
64
+ case "$SESSION_ID" in
65
+ ""|*[!A-Za-z0-9._-]*)
66
+ log "invalid session id: $SESSION_ID"
67
+ exit 0
68
+ ;;
69
+ esac
65
70
  { [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; } && exit 0
66
71
 
67
- LEGACY_CURSOR_FILE="/tmp/engram-cursor-${SESSION_ID}"
68
- CURSOR_FILE="/tmp/remnic-cursor-${SESSION_ID}"
69
- LEGACY_LOCK_DIR="/tmp/engram-lock-${SESSION_ID}.d"
70
- LOCK_DIR="/tmp/remnic-lock-${SESSION_ID}.d"
72
+ STATE_HOME="${XDG_STATE_HOME:-${HOME}/.local/state}"
73
+ STATE_DIR="${STATE_HOME}/remnic/hooks"
74
+
75
+ mkdir -p "$STATE_DIR" 2>/dev/null || exit 0
76
+ if ! node - "$STATE_DIR" <<'NODE'
77
+ const fs = require('fs');
78
+ const stateDir = process.argv[2];
79
+ try {
80
+ const info = fs.lstatSync(stateDir);
81
+ if (info.isSymbolicLink() || !info.isDirectory()) process.exit(1);
82
+ if (typeof process.getuid === 'function' && info.uid !== process.getuid()) process.exit(1);
83
+ if ((info.mode & 0o077) !== 0) fs.chmodSync(stateDir, 0o700);
84
+ } catch {
85
+ process.exit(1);
86
+ }
87
+ NODE
88
+ then
89
+ log "unsafe state directory $STATE_DIR"
90
+ exit 0
91
+ fi
92
+
93
+ CURSOR_FILE="${STATE_DIR}/remnic-cursor-${SESSION_ID}"
94
+ LOCK_DIR="${STATE_DIR}/remnic-lock-${SESSION_ID}.d"
95
+ LEGACY_CURSOR_FILE="${STATE_DIR}/engram-cursor-${SESSION_ID}"
96
+ LEGACY_LOCK_DIR="${STATE_DIR}/engram-lock-${SESSION_ID}.d"
71
97
 
72
98
  if [ ! -f "$CURSOR_FILE" ] && { [ -f "$LEGACY_CURSOR_FILE" ] || [ -d "$LEGACY_LOCK_DIR" ]; }; then
73
99
  CURSOR_FILE="$LEGACY_CURSOR_FILE"
74
100
  LOCK_DIR="$LEGACY_LOCK_DIR"
75
101
  fi
76
102
 
103
+ validate_cursor_file() {
104
+ node - "$CURSOR_FILE" <<'NODE'
105
+ const fs = require('fs');
106
+ const cursorFile = process.argv[2];
107
+ try {
108
+ const info = fs.lstatSync(cursorFile);
109
+ if (info.isSymbolicLink() || !info.isFile()) process.exit(1);
110
+ if (typeof process.getuid === 'function' && info.uid !== process.getuid()) process.exit(1);
111
+ } catch (error) {
112
+ if (error && error.code === 'ENOENT') process.exit(0);
113
+ process.exit(1);
114
+ }
115
+ NODE
116
+ }
117
+
118
+ read_cursor_file() {
119
+ validate_cursor_file || {
120
+ log "unsafe cursor file $CURSOR_FILE"
121
+ return 1
122
+ }
123
+ [ -f "$CURSOR_FILE" ] && cat "$CURSOR_FILE" 2>/dev/null || echo 0
124
+ }
125
+
126
+ write_cursor_file() {
127
+ NEW_CURSOR_VALUE="$1"
128
+ validate_cursor_file || {
129
+ log "refusing unsafe cursor file $CURSOR_FILE"
130
+ return 1
131
+ }
132
+ TEMP_CURSOR="$(mktemp "${CURSOR_FILE}.tmp.XXXXXX" 2>/dev/null)" || return 1
133
+ if ! printf '%s\n' "$NEW_CURSOR_VALUE" > "$TEMP_CURSOR"; then
134
+ rm -f "$TEMP_CURSOR"
135
+ return 1
136
+ fi
137
+ chmod 600 "$TEMP_CURSOR" 2>/dev/null || true
138
+ mv -f "$TEMP_CURSOR" "$CURSOR_FILE"
139
+ }
140
+
141
+ migrate_tmp_cursor_file() {
142
+ for TMP_CURSOR_FILE in "/tmp/remnic-cursor-${SESSION_ID}" "/tmp/engram-cursor-${SESSION_ID}"; do
143
+ [ ! -e "$TMP_CURSOR_FILE" ] && continue
144
+ TMP_CURSOR_VALUE="$(node - "$TMP_CURSOR_FILE" <<'NODE'
145
+ const fs = require('fs');
146
+ const cursorFile = process.argv[2];
147
+ try {
148
+ const info = fs.lstatSync(cursorFile);
149
+ if (info.isSymbolicLink() || !info.isFile()) process.exit(1);
150
+ if (typeof process.getuid === 'function' && info.uid !== process.getuid()) process.exit(1);
151
+ const value = fs.readFileSync(cursorFile, 'utf8').trim();
152
+ if (!/^\d+$/.test(value)) process.exit(1);
153
+ process.stdout.write(value);
154
+ } catch {
155
+ process.exit(1);
156
+ }
157
+ NODE
158
+ )" || continue
159
+ CURRENT_CURSOR_VALUE=""
160
+ if validate_cursor_file; then
161
+ CURRENT_CURSOR_VALUE="$([ -f "$CURSOR_FILE" ] && cat "$CURSOR_FILE" 2>/dev/null || echo "")"
162
+ fi
163
+ case "$CURRENT_CURSOR_VALUE" in
164
+ ""|*[!0-9]*) CURRENT_CURSOR_VALUE="-1" ;;
165
+ esac
166
+ if [ "$TMP_CURSOR_VALUE" -gt "$CURRENT_CURSOR_VALUE" ]; then
167
+ write_cursor_file "$TMP_CURSOR_VALUE" || continue
168
+ fi
169
+ rm -f "$TMP_CURSOR_FILE" 2>/dev/null
170
+ done
171
+ }
172
+
173
+ remove_stale_lock_dir() {
174
+ node - "$LOCK_DIR" <<'NODE'
175
+ const fs = require('fs');
176
+ const lockDir = process.argv[2];
177
+ try {
178
+ const info = fs.lstatSync(lockDir);
179
+ if (info.isSymbolicLink() || !info.isDirectory()) process.exit(1);
180
+ if (typeof process.getuid === 'function' && info.uid !== process.getuid()) process.exit(1);
181
+ if (Date.now() - info.mtimeMs < 10 * 60 * 1000) process.exit(0);
182
+ fs.rmSync(lockDir, { recursive: true, force: true });
183
+ } catch (error) {
184
+ if (error && error.code === 'ENOENT') process.exit(0);
185
+ process.exit(1);
186
+ }
187
+ NODE
188
+ }
189
+
77
190
  (
78
191
  # Acquire exclusive lock
79
192
  ACQUIRED=0
80
193
  for _i in $(seq 1 50); do
81
194
  if mkdir "$LOCK_DIR" 2>/dev/null; then ACQUIRED=1; break; fi
195
+ [ "$_i" -eq 1 ] && remove_stale_lock_dir >/dev/null 2>&1
82
196
  sleep 0.1
83
197
  done
84
198
  trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT INT TERM
85
199
  [ "$ACQUIRED" -eq 0 ] && exit 0
86
200
 
201
+ migrate_tmp_cursor_file
202
+
87
203
  LAST_COUNT=0
88
- [ -f "$CURSOR_FILE" ] && LAST_COUNT="$(cat "$CURSOR_FILE" 2>/dev/null || echo 0)"
204
+ LAST_COUNT="$(read_cursor_file)" || exit 0
89
205
 
90
206
  PAYLOAD="$(node -e "
91
207
  const fs = require('fs');
@@ -130,7 +246,7 @@ fi
130
246
  [ -z "$PAYLOAD" ] && { log "parse failed for $SESSION_ID"; exit 0; }
131
247
 
132
248
  if echo "$PAYLOAD" | grep -q "^CURSOR:"; then
133
- echo "${PAYLOAD#CURSOR:}" > "$CURSOR_FILE"
249
+ write_cursor_file "${PAYLOAD#CURSOR:}" || log "cursor write failed for $SESSION_ID"
134
250
  exit 0
135
251
  fi
136
252
 
@@ -153,7 +269,7 @@ fi
153
269
 
154
270
  if [ $CURL_EXIT -eq 0 ] && [[ "$HTTP_STATUS" =~ ^2 ]]; then
155
271
  log "observe OK for $SESSION_ID"
156
- echo "$TOTAL" > "$CURSOR_FILE"
272
+ write_cursor_file "$TOTAL" || log "cursor write failed for $SESSION_ID"
157
273
  else
158
274
  log "observe failed (curl=$CURL_EXIT http=$HTTP_STATUS) — cursor not advanced"
159
275
  fi
@@ -1,21 +1,38 @@
1
1
  #!/usr/bin/env bash
2
2
  # Remnic session cleanup for Claude Code.
3
- # Removes cursor and lock files for the session.
3
+ # Removes private cursor and lock files for the session.
4
4
  #
5
5
  # NOTE: Claude Code does not support a Stop/SessionEnd hook event.
6
6
  # This script is provided for manual cleanup or future hook support.
7
- # Temp files in /tmp/ are cleaned by the OS on reboot.
7
+ # Private state files live under ${XDG_STATE_HOME:-$HOME/.local/state}/remnic/hooks.
8
8
 
9
9
  INPUT="$(cat)"
10
10
  SESSION_ID="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.session_id||'')" "$INPUT" 2>/dev/null || echo "")"
11
11
 
12
12
  echo '{"continue":true}'
13
13
 
14
- [ -z "$SESSION_ID" ] && exit 0
14
+ case "$SESSION_ID" in
15
+ ""|*[!A-Za-z0-9._-]*)
16
+ exit 0
17
+ ;;
18
+ esac
15
19
 
16
- rm -f "/tmp/remnic-cursor-${SESSION_ID}" 2>/dev/null
17
- rmdir "/tmp/remnic-lock-${SESSION_ID}.d" 2>/dev/null
18
- rm -f "/tmp/engram-cursor-${SESSION_ID}" 2>/dev/null
19
- rmdir "/tmp/engram-lock-${SESSION_ID}.d" 2>/dev/null
20
+ STATE_HOME="${XDG_STATE_HOME:-${HOME}/.local/state}"
21
+ STATE_DIR="${STATE_HOME}/remnic/hooks"
22
+ CURSOR_FILE="${STATE_DIR}/remnic-cursor-${SESSION_ID}"
23
+ LOCK_DIR="${STATE_DIR}/remnic-lock-${SESSION_ID}.d"
24
+ LEGACY_CURSOR_FILE="${STATE_DIR}/engram-cursor-${SESSION_ID}"
25
+ LEGACY_LOCK_DIR="${STATE_DIR}/engram-lock-${SESSION_ID}.d"
26
+
27
+ if [ -d "$STATE_DIR" ] && [ ! -L "$STATE_DIR" ]; then
28
+ if [ -e "$CURSOR_FILE" ] && [ ! -L "$CURSOR_FILE" ]; then
29
+ rm -f "$CURSOR_FILE" 2>/dev/null
30
+ fi
31
+ rmdir "$LOCK_DIR" 2>/dev/null
32
+ if [ -e "$LEGACY_CURSOR_FILE" ] && [ ! -L "$LEGACY_CURSOR_FILE" ]; then
33
+ rm -f "$LEGACY_CURSOR_FILE" 2>/dev/null
34
+ fi
35
+ rmdir "$LEGACY_LOCK_DIR" 2>/dev/null
36
+ fi
20
37
 
21
38
  exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/plugin-claude-code",
3
- "version": "9.3.572",
3
+ "version": "9.3.573",
4
4
  "description": "Remnic memory plugin for Claude Code — hooks, skills, MCP integration",
5
5
  "type": "module",
6
6
  "license": "MIT",