@remnic/plugin-codex 9.3.572 → 9.3.574

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.
@@ -62,31 +62,147 @@ CWD="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.cwd|
62
62
  TOOL_NAME="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.tool_name||'')" "$INPUT" 2>/dev/null || echo "")"
63
63
  PROJECT_NAME="$(basename "$CWD" 2>/dev/null || echo "unknown")"
64
64
 
65
- [ -z "$SESSION_ID" ] && exit 0
65
+ case "$SESSION_ID" in
66
+ ""|*[!A-Za-z0-9._-]*)
67
+ log "invalid session id: $SESSION_ID"
68
+ exit 0
69
+ ;;
70
+ esac
66
71
  { [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; } && exit 0
67
72
 
68
- LEGACY_CURSOR_FILE="/tmp/engram-cursor-${SESSION_ID}"
69
- CURSOR_FILE="/tmp/remnic-cursor-${SESSION_ID}"
70
- LEGACY_LOCK_DIR="/tmp/engram-lock-${SESSION_ID}.d"
71
- LOCK_DIR="/tmp/remnic-lock-${SESSION_ID}.d"
73
+ STATE_HOME="${XDG_STATE_HOME:-${HOME}/.local/state}"
74
+ STATE_DIR="${STATE_HOME}/remnic/hooks"
75
+
76
+ mkdir -p "$STATE_DIR" 2>/dev/null || exit 0
77
+ if ! node - "$STATE_DIR" <<'NODE'
78
+ const fs = require('fs');
79
+ const stateDir = process.argv[2];
80
+ try {
81
+ const info = fs.lstatSync(stateDir);
82
+ if (info.isSymbolicLink() || !info.isDirectory()) process.exit(1);
83
+ if (typeof process.getuid === 'function' && info.uid !== process.getuid()) process.exit(1);
84
+ if ((info.mode & 0o077) !== 0) fs.chmodSync(stateDir, 0o700);
85
+ } catch {
86
+ process.exit(1);
87
+ }
88
+ NODE
89
+ then
90
+ log "unsafe state directory $STATE_DIR"
91
+ exit 0
92
+ fi
93
+
94
+ CURSOR_FILE="${STATE_DIR}/remnic-cursor-${SESSION_ID}"
95
+ LOCK_DIR="${STATE_DIR}/remnic-lock-${SESSION_ID}.d"
96
+ LEGACY_CURSOR_FILE="${STATE_DIR}/engram-cursor-${SESSION_ID}"
97
+ LEGACY_LOCK_DIR="${STATE_DIR}/engram-lock-${SESSION_ID}.d"
72
98
 
73
99
  if [ ! -f "$CURSOR_FILE" ] && { [ -f "$LEGACY_CURSOR_FILE" ] || [ -d "$LEGACY_LOCK_DIR" ]; }; then
74
100
  CURSOR_FILE="$LEGACY_CURSOR_FILE"
75
101
  LOCK_DIR="$LEGACY_LOCK_DIR"
76
102
  fi
77
103
 
104
+ validate_cursor_file() {
105
+ node - "$CURSOR_FILE" <<'NODE'
106
+ const fs = require('fs');
107
+ const cursorFile = process.argv[2];
108
+ try {
109
+ const info = fs.lstatSync(cursorFile);
110
+ if (info.isSymbolicLink() || !info.isFile()) process.exit(1);
111
+ if (typeof process.getuid === 'function' && info.uid !== process.getuid()) process.exit(1);
112
+ } catch (error) {
113
+ if (error && error.code === 'ENOENT') process.exit(0);
114
+ process.exit(1);
115
+ }
116
+ NODE
117
+ }
118
+
119
+ read_cursor_file() {
120
+ validate_cursor_file || {
121
+ log "unsafe cursor file $CURSOR_FILE"
122
+ return 1
123
+ }
124
+ [ -f "$CURSOR_FILE" ] && cat "$CURSOR_FILE" 2>/dev/null || echo 0
125
+ }
126
+
127
+ write_cursor_file() {
128
+ NEW_CURSOR_VALUE="$1"
129
+ validate_cursor_file || {
130
+ log "refusing unsafe cursor file $CURSOR_FILE"
131
+ return 1
132
+ }
133
+ TEMP_CURSOR="$(mktemp "${CURSOR_FILE}.tmp.XXXXXX" 2>/dev/null)" || return 1
134
+ if ! printf '%s\n' "$NEW_CURSOR_VALUE" > "$TEMP_CURSOR"; then
135
+ rm -f "$TEMP_CURSOR"
136
+ return 1
137
+ fi
138
+ chmod 600 "$TEMP_CURSOR" 2>/dev/null || true
139
+ mv -f "$TEMP_CURSOR" "$CURSOR_FILE"
140
+ }
141
+
142
+ migrate_tmp_cursor_file() {
143
+ for TMP_CURSOR_FILE in "/tmp/remnic-cursor-${SESSION_ID}" "/tmp/engram-cursor-${SESSION_ID}"; do
144
+ [ ! -e "$TMP_CURSOR_FILE" ] && continue
145
+ TMP_CURSOR_VALUE="$(node - "$TMP_CURSOR_FILE" <<'NODE'
146
+ const fs = require('fs');
147
+ const cursorFile = process.argv[2];
148
+ try {
149
+ const info = fs.lstatSync(cursorFile);
150
+ if (info.isSymbolicLink() || !info.isFile()) process.exit(1);
151
+ if (typeof process.getuid === 'function' && info.uid !== process.getuid()) process.exit(1);
152
+ const value = fs.readFileSync(cursorFile, 'utf8').trim();
153
+ if (!/^\d+$/.test(value)) process.exit(1);
154
+ process.stdout.write(value);
155
+ } catch {
156
+ process.exit(1);
157
+ }
158
+ NODE
159
+ )" || continue
160
+ CURRENT_CURSOR_VALUE=""
161
+ if validate_cursor_file; then
162
+ CURRENT_CURSOR_VALUE="$([ -f "$CURSOR_FILE" ] && cat "$CURSOR_FILE" 2>/dev/null || echo "")"
163
+ fi
164
+ case "$CURRENT_CURSOR_VALUE" in
165
+ ""|*[!0-9]*) CURRENT_CURSOR_VALUE="-1" ;;
166
+ esac
167
+ if [ "$TMP_CURSOR_VALUE" -gt "$CURRENT_CURSOR_VALUE" ]; then
168
+ write_cursor_file "$TMP_CURSOR_VALUE" || continue
169
+ fi
170
+ rm -f "$TMP_CURSOR_FILE" 2>/dev/null
171
+ done
172
+ }
173
+
174
+ remove_stale_lock_dir() {
175
+ node - "$LOCK_DIR" <<'NODE'
176
+ const fs = require('fs');
177
+ const lockDir = process.argv[2];
178
+ try {
179
+ const info = fs.lstatSync(lockDir);
180
+ if (info.isSymbolicLink() || !info.isDirectory()) process.exit(1);
181
+ if (typeof process.getuid === 'function' && info.uid !== process.getuid()) process.exit(1);
182
+ if (Date.now() - info.mtimeMs < 10 * 60 * 1000) process.exit(0);
183
+ fs.rmSync(lockDir, { recursive: true, force: true });
184
+ } catch (error) {
185
+ if (error && error.code === 'ENOENT') process.exit(0);
186
+ process.exit(1);
187
+ }
188
+ NODE
189
+ }
190
+
78
191
  (
79
192
  # Acquire exclusive lock
80
193
  ACQUIRED=0
81
194
  for _i in $(seq 1 50); do
82
195
  if mkdir "$LOCK_DIR" 2>/dev/null; then ACQUIRED=1; break; fi
196
+ [ "$_i" -eq 1 ] && remove_stale_lock_dir >/dev/null 2>&1
83
197
  sleep 0.1
84
198
  done
85
199
  trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT INT TERM
86
200
  [ "$ACQUIRED" -eq 0 ] && exit 0
87
201
 
202
+ migrate_tmp_cursor_file
203
+
88
204
  LAST_COUNT=0
89
- [ -f "$CURSOR_FILE" ] && LAST_COUNT="$(cat "$CURSOR_FILE" 2>/dev/null || echo 0)"
205
+ LAST_COUNT="$(read_cursor_file)" || exit 0
90
206
 
91
207
  PAYLOAD="$(node -e "
92
208
  const fs = require('fs');
@@ -131,7 +247,7 @@ fi
131
247
  [ -z "$PAYLOAD" ] && { log "parse failed for $SESSION_ID"; exit 0; }
132
248
 
133
249
  if echo "$PAYLOAD" | grep -q "^CURSOR:"; then
134
- echo "${PAYLOAD#CURSOR:}" > "$CURSOR_FILE"
250
+ write_cursor_file "${PAYLOAD#CURSOR:}" || log "cursor write failed for $SESSION_ID"
135
251
  exit 0
136
252
  fi
137
253
 
@@ -154,7 +270,7 @@ fi
154
270
 
155
271
  if [ $CURL_EXIT -eq 0 ] && [[ "$HTTP_STATUS" =~ ^2 ]]; then
156
272
  log "observe OK for $SESSION_ID"
157
- echo "$TOTAL" > "$CURSOR_FILE"
273
+ write_cursor_file "$TOTAL" || log "cursor write failed for $SESSION_ID"
158
274
  else
159
275
  log "observe failed (curl=$CURL_EXIT http=$HTTP_STATUS) — cursor not advanced"
160
276
  fi
@@ -53,55 +53,172 @@ TRANSCRIPT_PATH="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.
53
53
 
54
54
  echo '{"continue":true}'
55
55
 
56
- # Final observe flush if we have transcript
57
- if [ -n "$REMNIC_TOKEN" ] && [ -n "$SESSION_ID" ] && [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
58
- LEGACY_CURSOR_FILE="/tmp/engram-cursor-${SESSION_ID}"
59
- CURSOR_FILE="/tmp/remnic-cursor-${SESSION_ID}"
60
- if [ ! -f "$CURSOR_FILE" ] && [ -f "$LEGACY_CURSOR_FILE" ]; then
56
+ SESSION_ID_SAFE=0
57
+ if [[ "$SESSION_ID" != "" && ! "$SESSION_ID" =~ [^A-Za-z0-9._-] ]]; then
58
+ SESSION_ID_SAFE=1
59
+ STATE_HOME="${XDG_STATE_HOME:-${HOME}/.local/state}"
60
+ STATE_DIR="${STATE_HOME}/remnic/hooks"
61
+ CURSOR_FILE="${STATE_DIR}/remnic-cursor-${SESSION_ID}"
62
+ LOCK_DIR="${STATE_DIR}/remnic-lock-${SESSION_ID}.d"
63
+ LEGACY_CURSOR_FILE="${STATE_DIR}/engram-cursor-${SESSION_ID}"
64
+ LEGACY_LOCK_DIR="${STATE_DIR}/engram-lock-${SESSION_ID}.d"
65
+
66
+ mkdir -p "$STATE_DIR" 2>/dev/null || SESSION_ID_SAFE=0
67
+ if [ "$SESSION_ID_SAFE" -eq 1 ]; then
68
+ if ! node - "$STATE_DIR" <<'NODE'
69
+ const fs = require('fs');
70
+ const stateDir = process.argv[2];
71
+ try {
72
+ const info = fs.lstatSync(stateDir);
73
+ if (info.isSymbolicLink() || !info.isDirectory()) process.exit(1);
74
+ if (typeof process.getuid === 'function' && info.uid !== process.getuid()) process.exit(1);
75
+ if ((info.mode & 0o077) !== 0) fs.chmodSync(stateDir, 0o700);
76
+ } catch {
77
+ process.exit(1);
78
+ }
79
+ NODE
80
+ then
81
+ log "unsafe state directory $STATE_DIR"
82
+ SESSION_ID_SAFE=0
83
+ fi
84
+ fi
85
+ if [ "$SESSION_ID_SAFE" -eq 1 ] && [ ! -f "$CURSOR_FILE" ] && { [ -f "$LEGACY_CURSOR_FILE" ] || [ -d "$LEGACY_LOCK_DIR" ]; }; then
61
86
  CURSOR_FILE="$LEGACY_CURSOR_FILE"
87
+ LOCK_DIR="$LEGACY_LOCK_DIR"
62
88
  fi
89
+ fi
90
+
91
+ validate_cursor_file() {
92
+ node - "$CURSOR_FILE" <<'NODE'
93
+ const fs = require('fs');
94
+ const cursorFile = process.argv[2];
95
+ try {
96
+ const info = fs.lstatSync(cursorFile);
97
+ if (info.isSymbolicLink() || !info.isFile()) process.exit(1);
98
+ if (typeof process.getuid === 'function' && info.uid !== process.getuid()) process.exit(1);
99
+ } catch (error) {
100
+ if (error && error.code === 'ENOENT') process.exit(0);
101
+ process.exit(1);
102
+ }
103
+ NODE
104
+ }
105
+
106
+ read_cursor_file() {
107
+ validate_cursor_file || {
108
+ log "unsafe cursor file $CURSOR_FILE"
109
+ return 1
110
+ }
111
+ [ -f "$CURSOR_FILE" ] && cat "$CURSOR_FILE" 2>/dev/null || echo 0
112
+ }
113
+
114
+ write_cursor_file() {
115
+ NEW_CURSOR_VALUE="$1"
116
+ validate_cursor_file || {
117
+ log "refusing unsafe cursor file $CURSOR_FILE"
118
+ return 1
119
+ }
120
+ TEMP_CURSOR="$(mktemp "${CURSOR_FILE}.tmp.XXXXXX" 2>/dev/null)" || return 1
121
+ if ! printf '%s\n' "$NEW_CURSOR_VALUE" > "$TEMP_CURSOR"; then
122
+ rm -f "$TEMP_CURSOR"
123
+ return 1
124
+ fi
125
+ chmod 600 "$TEMP_CURSOR" 2>/dev/null || true
126
+ mv -f "$TEMP_CURSOR" "$CURSOR_FILE"
127
+ }
128
+
129
+ migrate_tmp_cursor_file() {
130
+ for TMP_CURSOR_FILE in "/tmp/remnic-cursor-${SESSION_ID}" "/tmp/engram-cursor-${SESSION_ID}"; do
131
+ [ ! -e "$TMP_CURSOR_FILE" ] && continue
132
+ TMP_CURSOR_VALUE="$(node - "$TMP_CURSOR_FILE" <<'NODE'
133
+ const fs = require('fs');
134
+ const cursorFile = process.argv[2];
135
+ try {
136
+ const info = fs.lstatSync(cursorFile);
137
+ if (info.isSymbolicLink() || !info.isFile()) process.exit(1);
138
+ if (typeof process.getuid === 'function' && info.uid !== process.getuid()) process.exit(1);
139
+ const value = fs.readFileSync(cursorFile, 'utf8').trim();
140
+ if (!/^\d+$/.test(value)) process.exit(1);
141
+ process.stdout.write(value);
142
+ } catch {
143
+ process.exit(1);
144
+ }
145
+ NODE
146
+ )" || continue
147
+ CURRENT_CURSOR_VALUE=""
148
+ if validate_cursor_file; then
149
+ CURRENT_CURSOR_VALUE="$([ -f "$CURSOR_FILE" ] && cat "$CURSOR_FILE" 2>/dev/null || echo "")"
150
+ fi
151
+ case "$CURRENT_CURSOR_VALUE" in
152
+ ""|*[!0-9]*) CURRENT_CURSOR_VALUE="-1" ;;
153
+ esac
154
+ if [ "$TMP_CURSOR_VALUE" -gt "$CURRENT_CURSOR_VALUE" ]; then
155
+ write_cursor_file "$TMP_CURSOR_VALUE" || continue
156
+ fi
157
+ rm -f "$TMP_CURSOR_FILE" 2>/dev/null
158
+ done
159
+ }
160
+
161
+ remove_cursor_file() {
162
+ validate_cursor_file || {
163
+ log "refusing unsafe cursor file $CURSOR_FILE"
164
+ return 1
165
+ }
166
+ rm -f "$CURSOR_FILE"
167
+ }
168
+
169
+ # Final observe flush if we have transcript
170
+ if [ -n "$REMNIC_TOKEN" ] && [ "$SESSION_ID_SAFE" -eq 1 ] && [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
63
171
  LAST_COUNT=0
64
- [ -f "$CURSOR_FILE" ] && LAST_COUNT="$(cat "$CURSOR_FILE" 2>/dev/null || echo 0)"
172
+ migrate_tmp_cursor_file
173
+ if LAST_COUNT="$(read_cursor_file)"; then
174
+ PAYLOAD="$(node -e "
175
+ const fs = require('fs');
176
+ const lines = fs.readFileSync(process.argv[1], 'utf8').split('\n').filter(Boolean);
177
+ const messages = [];
178
+ for (const line of lines) {
179
+ try {
180
+ const entry = JSON.parse(line);
181
+ if (entry.type !== 'user' && entry.type !== 'assistant') continue;
182
+ const msg = entry.message;
183
+ if (!msg || typeof msg !== 'object') continue;
184
+ const role = msg.role;
185
+ if (role !== 'user' && role !== 'assistant') continue;
186
+ let text = typeof msg.content === 'string' ? msg.content.trim() :
187
+ Array.isArray(msg.content) ? msg.content.filter(b => b.type === 'text' && b.text).map(b => b.text.trim()).join('\n').trim() : '';
188
+ if (text) messages.push({ role, content: text });
189
+ } catch {}
190
+ }
191
+ const newMessages = messages.slice(parseInt(process.argv[3], 10) || 0);
192
+ if (newMessages.length) {
193
+ process.stdout.write(JSON.stringify({ sessionKey: process.argv[2], messages: newMessages }));
194
+ }
195
+ " "$TRANSCRIPT_PATH" "$SESSION_ID" "$LAST_COUNT" 2>/dev/null)"
65
196
 
66
- PAYLOAD="$(node -e "
67
- const fs = require('fs');
68
- const lines = fs.readFileSync(process.argv[1], 'utf8').split('\n').filter(Boolean);
69
- const messages = [];
70
- for (const line of lines) {
71
- try {
72
- const entry = JSON.parse(line);
73
- if (entry.type !== 'user' && entry.type !== 'assistant') continue;
74
- const msg = entry.message;
75
- if (!msg || typeof msg !== 'object') continue;
76
- const role = msg.role;
77
- if (role !== 'user' && role !== 'assistant') continue;
78
- let text = typeof msg.content === 'string' ? msg.content.trim() :
79
- Array.isArray(msg.content) ? msg.content.filter(b => b.type === 'text' && b.text).map(b => b.text.trim()).join('\n').trim() : '';
80
- if (text) messages.push({ role, content: text });
81
- } catch {}
82
- }
83
- const newMessages = messages.slice(parseInt(process.argv[3], 10) || 0);
84
- if (newMessages.length) {
85
- process.stdout.write(JSON.stringify({ sessionKey: process.argv[2], messages: newMessages }));
86
- }
87
- " "$TRANSCRIPT_PATH" "$SESSION_ID" "$LAST_COUNT" 2>/dev/null)"
88
-
89
- if [ -n "$PAYLOAD" ]; then
90
- log "final flush for $SESSION_ID"
91
- curl -s --max-time 30 \
92
- -X POST "$REMNIC_URL" \
93
- -H "Authorization: Bearer ${REMNIC_TOKEN}" \
94
- -H "Content-Type: application/json" \
95
- -H "X-Engram-Client-Id: codex" \
96
- -d "$PAYLOAD" >/dev/null 2>&1 || log "final flush failed"
197
+ if [ -n "$PAYLOAD" ]; then
198
+ log "final flush for $SESSION_ID"
199
+ curl -s --max-time 30 \
200
+ -X POST "$REMNIC_URL" \
201
+ -H "Authorization: Bearer ${REMNIC_TOKEN}" \
202
+ -H "Content-Type: application/json" \
203
+ -H "X-Engram-Client-Id: codex" \
204
+ -d "$PAYLOAD" >/dev/null 2>&1 || log "final flush failed"
205
+ fi
206
+ else
207
+ log "final flush skipped for $SESSION_ID due to unsafe cursor"
97
208
  fi
98
209
  fi
99
210
 
100
211
  # Cleanup
101
- rm -f "/tmp/remnic-cursor-${SESSION_ID}" 2>/dev/null
102
- rmdir "/tmp/remnic-lock-${SESSION_ID}.d" 2>/dev/null
103
- rm -f "/tmp/engram-cursor-${SESSION_ID}" 2>/dev/null
104
- rmdir "/tmp/engram-lock-${SESSION_ID}.d" 2>/dev/null
212
+ if [ "$SESSION_ID_SAFE" -eq 1 ]; then
213
+ remove_cursor_file || true
214
+ rmdir "$LOCK_DIR" 2>/dev/null || true
215
+ if [ "$CURSOR_FILE" != "$LEGACY_CURSOR_FILE" ] && [ -e "$LEGACY_CURSOR_FILE" ] && [ ! -L "$LEGACY_CURSOR_FILE" ]; then
216
+ rm -f "$LEGACY_CURSOR_FILE" 2>/dev/null || true
217
+ fi
218
+ if [ "$LOCK_DIR" != "$LEGACY_LOCK_DIR" ]; then
219
+ rmdir "$LEGACY_LOCK_DIR" 2>/dev/null || true
220
+ fi
221
+ fi
105
222
 
106
223
  # Codex-native memory materialization (#378). The script honors the
107
224
  # `codexMaterializeMemories` config flag and the `.remnic-managed` sentinel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/plugin-codex",
3
- "version": "9.3.572",
3
+ "version": "9.3.574",
4
4
  "description": "Remnic memory plugin for Codex CLI — hooks, skills, MCP integration",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,6 +30,6 @@
30
30
  ".mcp.json"
31
31
  ],
32
32
  "dependencies": {
33
- "@remnic/core": "^9.3.572"
33
+ "@remnic/core": "^9.3.574"
34
34
  }
35
35
  }