@remnic/plugin-codex 9.3.612 → 9.3.614

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,280 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Remnic PostToolUse hook for Codex.
3
- # Observes Bash tool executions by sending transcript delta to the
4
- # observe endpoint. Runs in background, never blocks.
5
-
6
- set -euo pipefail
7
-
8
- ensure_migrated() {
9
- if [ -f "${HOME}/.remnic/.migrated-from-engram" ]; then
10
- return 0
11
- fi
12
- if [ ! -d "${HOME}/.engram" ] && [ ! -f "${HOME}/.config/engram/config.json" ]; then
13
- return 0
14
- fi
15
- if command -v remnic >/dev/null 2>&1; then
16
- remnic migrate >/dev/null 2>&1 || true
17
- elif command -v engram >/dev/null 2>&1; then
18
- engram migrate >/dev/null 2>&1 || true
19
- fi
20
- }
21
-
22
- ensure_migrated
23
-
24
- REMNIC_HOST="${REMNIC_HOST:-${ENGRAM_HOST:-127.0.0.1}}"
25
- REMNIC_PORT="${REMNIC_PORT:-${ENGRAM_PORT:-4318}}"
26
- REMNIC_URL="http://${REMNIC_HOST}:${REMNIC_PORT}/engram/v1/observe"
27
-
28
- LOG="${HOME}/.remnic/logs/remnic-post-tool-observe.log"
29
- mkdir -p "$(dirname "$LOG")"
30
- log() { echo "$(date '+%F %T') [codex-post-tool] $*" >> "$LOG"; }
31
-
32
- # Read token
33
- REMNIC_TOKEN=""
34
- for TOKEN_FILE in "${HOME}/.remnic/tokens.json" "${HOME}/.engram/tokens.json"; do
35
- [ ! -f "$TOKEN_FILE" ] && continue
36
- REMNIC_TOKEN="$(node -e "
37
- const fs = require('fs');
38
- const tokenFile = process.argv[1];
39
- const store = JSON.parse(fs.readFileSync(tokenFile, 'utf8'));
40
- const tokens = store.tokens || [];
41
- const cxc = tokens.find(t => t.connector === 'codex-cli');
42
- const cx = tokens.find(t => t.connector === 'codex');
43
- const oc = tokens.find(t => t.connector === 'openclaw');
44
- let tok = (cxc && cxc.token) || (cx && cx.token) || (oc && oc.token) || '';
45
- if (!tok) { tok = store['codex-cli'] || store['codex'] || store['openclaw'] || ''; }
46
- process.stdout.write(tok);
47
- " "$TOKEN_FILE" 2>/dev/null || echo "")"
48
- [ -n "$REMNIC_TOKEN" ] && break
49
- done
50
- [ -z "$REMNIC_TOKEN" ] && REMNIC_TOKEN="${OPENCLAW_REMNIC_ACCESS_TOKEN:-${OPENCLAW_ENGRAM_ACCESS_TOKEN:-}}"
51
-
52
- INPUT="$(cat)"
53
-
54
- # Return immediately — never block the tool
55
- echo '{"continue":true}'
56
-
57
- [ -z "$REMNIC_TOKEN" ] && exit 0
58
-
59
- SESSION_ID="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.session_id||'')" "$INPUT" 2>/dev/null || echo "")"
60
- TRANSCRIPT_PATH="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.transcript_path||'')" "$INPUT" 2>/dev/null || echo "")"
61
- CWD="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.cwd||'')" "$INPUT" 2>/dev/null || echo "")"
62
- TOOL_NAME="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.tool_name||'')" "$INPUT" 2>/dev/null || echo "")"
63
- PROJECT_NAME="$(basename "$CWD" 2>/dev/null || echo "unknown")"
64
-
65
- case "$SESSION_ID" in
66
- ""|*[!A-Za-z0-9._-]*)
67
- log "invalid session id: $SESSION_ID"
68
- exit 0
69
- ;;
70
- esac
71
- { [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; } && exit 0
72
-
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"
98
-
99
- if [ ! -f "$CURSOR_FILE" ] && { [ -f "$LEGACY_CURSOR_FILE" ] || [ -d "$LEGACY_LOCK_DIR" ]; }; then
100
- CURSOR_FILE="$LEGACY_CURSOR_FILE"
101
- LOCK_DIR="$LEGACY_LOCK_DIR"
102
- fi
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
-
191
- (
192
- # Acquire exclusive lock
193
- ACQUIRED=0
194
- for _i in $(seq 1 50); do
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
197
- sleep 0.1
198
- done
199
- trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT INT TERM
200
- [ "$ACQUIRED" -eq 0 ] && exit 0
201
-
202
- migrate_tmp_cursor_file
203
-
204
- LAST_COUNT=0
205
- LAST_COUNT="$(read_cursor_file)" || exit 0
206
-
207
- PAYLOAD="$(node -e "
208
- const fs = require('fs');
209
- const path = process.argv[1];
210
- const sessionId = process.argv[2];
211
- const lastCount = parseInt(process.argv[3], 10) || 0;
212
-
213
- const lines = fs.readFileSync(path, 'utf8').split('\n').filter(Boolean);
214
- const messages = [];
215
- for (const line of lines) {
216
- try {
217
- const entry = JSON.parse(line);
218
- if (entry.type !== 'user' && entry.type !== 'assistant') continue;
219
- const msg = entry.message;
220
- if (!msg || typeof msg !== 'object') continue;
221
- const role = msg.role;
222
- if (role !== 'user' && role !== 'assistant') continue;
223
- let text = '';
224
- if (typeof msg.content === 'string') text = msg.content.trim();
225
- else if (Array.isArray(msg.content)) {
226
- text = msg.content
227
- .filter(b => b.type === 'text' && b.text)
228
- .map(b => b.text.trim())
229
- .join('\n').trim();
230
- }
231
- if (text) messages.push({ role, content: text });
232
- } catch {}
233
- }
234
-
235
- const newMessages = messages.slice(lastCount);
236
- if (!newMessages.length) {
237
- process.stdout.write('CURSOR:' + messages.length);
238
- } else {
239
- process.stdout.write(JSON.stringify({
240
- sessionKey: sessionId,
241
- messages: newMessages,
242
- __total__: messages.length
243
- }));
244
- }
245
- " "$TRANSCRIPT_PATH" "$SESSION_ID" "$LAST_COUNT" 2>/dev/null)"
246
-
247
- [ -z "$PAYLOAD" ] && { log "parse failed for $SESSION_ID"; exit 0; }
248
-
249
- if echo "$PAYLOAD" | grep -q "^CURSOR:"; then
250
- write_cursor_file "${PAYLOAD#CURSOR:}" || log "cursor write failed for $SESSION_ID"
251
- exit 0
252
- fi
253
-
254
- TOTAL="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(String(d.__total__||0))" "$PAYLOAD" 2>/dev/null || echo 0)"
255
- MSG_COUNT="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(String((d.messages||[]).length))" "$PAYLOAD" 2>/dev/null || echo "?")"
256
- CLEAN="$(node -e "const d=JSON.parse(process.argv[1]); delete d.__total__; process.stdout.write(JSON.stringify(d))" "$PAYLOAD" 2>/dev/null)"
257
-
258
- [ -z "$CLEAN" ] && exit 0
259
-
260
- log "observing $MSG_COUNT new messages (cursor $LAST_COUNT->$TOTAL) project=$PROJECT_NAME tool=$TOOL_NAME"
261
-
262
- RAW="$(curl -s -w "\n%{http_code}" --max-time 120 \
263
- -X POST "$REMNIC_URL" \
264
- -H "Authorization: Bearer ${REMNIC_TOKEN}" \
265
- -H "Content-Type: application/json" \
266
- -H "X-Engram-Client-Id: codex" \
267
- -d "$CLEAN" 2>/dev/null)"
268
- CURL_EXIT=$?
269
- HTTP_STATUS="$(echo "$RAW" | tail -1)"
270
-
271
- if [ $CURL_EXIT -eq 0 ] && [[ "$HTTP_STATUS" =~ ^2 ]]; then
272
- log "observe OK for $SESSION_ID"
273
- write_cursor_file "$TOTAL" || log "cursor write failed for $SESSION_ID"
274
- else
275
- log "observe failed (curl=$CURL_EXIT http=$HTTP_STATUS) — cursor not advanced"
276
- fi
277
- ) >> "$LOG" 2>&1 &
278
-
279
- disown $!
280
- exit 0
@@ -1,281 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Remnic Stop hook for Codex.
3
- # Performs final observe flush then cleans up cursor/lock files.
4
-
5
- set -euo pipefail
6
-
7
- ensure_migrated() {
8
- if [ -f "${HOME}/.remnic/.migrated-from-engram" ]; then
9
- return 0
10
- fi
11
- if [ ! -d "${HOME}/.engram" ] && [ ! -f "${HOME}/.config/engram/config.json" ]; then
12
- return 0
13
- fi
14
- if command -v remnic >/dev/null 2>&1; then
15
- remnic migrate >/dev/null 2>&1 || true
16
- elif command -v engram >/dev/null 2>&1; then
17
- engram migrate >/dev/null 2>&1 || true
18
- fi
19
- }
20
-
21
- ensure_migrated
22
-
23
- REMNIC_HOST="${REMNIC_HOST:-${ENGRAM_HOST:-127.0.0.1}}"
24
- REMNIC_PORT="${REMNIC_PORT:-${ENGRAM_PORT:-4318}}"
25
- REMNIC_URL="http://${REMNIC_HOST}:${REMNIC_PORT}/engram/v1/observe"
26
-
27
- LOG="${HOME}/.remnic/logs/remnic-codex-session-end.log"
28
- mkdir -p "$(dirname "$LOG")"
29
- log() { echo "$(date '+%F %T') [codex-stop] $*" >> "$LOG"; }
30
-
31
- REMNIC_TOKEN=""
32
- for TOKEN_FILE in "${HOME}/.remnic/tokens.json" "${HOME}/.engram/tokens.json"; do
33
- [ ! -f "$TOKEN_FILE" ] && continue
34
- REMNIC_TOKEN="$(node -e "
35
- const fs = require('fs');
36
- const tokenFile = process.argv[1];
37
- const store = JSON.parse(fs.readFileSync(tokenFile, 'utf8'));
38
- const tokens = store.tokens || [];
39
- const cxc = tokens.find(t => t.connector === 'codex-cli');
40
- const cx = tokens.find(t => t.connector === 'codex');
41
- const oc = tokens.find(t => t.connector === 'openclaw');
42
- let tok = (cxc && cxc.token) || (cx && cx.token) || (oc && oc.token) || '';
43
- if (!tok) { tok = store['codex-cli'] || store['codex'] || store['openclaw'] || ''; }
44
- process.stdout.write(tok);
45
- " "$TOKEN_FILE" 2>/dev/null || echo "")"
46
- [ -n "$REMNIC_TOKEN" ] && break
47
- done
48
- [ -z "$REMNIC_TOKEN" ] && REMNIC_TOKEN="${OPENCLAW_REMNIC_ACCESS_TOKEN:-${OPENCLAW_ENGRAM_ACCESS_TOKEN:-}}"
49
-
50
- INPUT="$(cat)"
51
- SESSION_ID="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.session_id||'')" "$INPUT" 2>/dev/null || echo "")"
52
- TRANSCRIPT_PATH="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.transcript_path||'')" "$INPUT" 2>/dev/null || echo "")"
53
-
54
- echo '{"continue":true}'
55
-
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
86
- CURSOR_FILE="$LEGACY_CURSOR_FILE"
87
- LOCK_DIR="$LEGACY_LOCK_DIR"
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
171
- LAST_COUNT=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)"
196
-
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"
208
- fi
209
- fi
210
-
211
- # Cleanup
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
222
-
223
- # Codex-native memory materialization (#378). The script honors the
224
- # `codexMaterializeMemories` config flag and the `.remnic-managed` sentinel,
225
- # so it's safe to run unconditionally here.
226
- #
227
- # Entrypoint-resolution order (first hit wins):
228
- # 1. $REMNIC_CODEX_MATERIALIZE_BIN — explicit override from the environment.
229
- # Set this to point at a custom Node wrapper if you need to short-circuit
230
- # the search order.
231
- # 2. The packaged CJS wrapper shipped with @remnic/plugin-codex at
232
- # `packages/plugin-codex/bin/materialize.cjs`, resolved relative to this
233
- # hook's own filesystem location. This is the preferred path for
234
- # published installs — the wrapper imports `@remnic/core` directly and
235
- # has zero dependency on the source tree. We resolve the path via
236
- # `BASH_SOURCE[0]` + `cd -P` so symlinked checkouts (pnpm hoisted
237
- # installs, worktree copies) land on the real file.
238
- # 3. Dev fallback: `scripts/codex-materialize.ts` at the repo root. Only
239
- # present in source checkouts — we use it when the packaged bin isn't
240
- # available, so local developers keep working without having to run a
241
- # build first. See PR #392 review thread PRRT_kwDORJXyws56TOVo for why
242
- # we can't rely on this in distributed installs.
243
- # 4. If neither yielded a usable path, we log the miss and exit the block
244
- # without running the materializer. A verbose log line is strictly
245
- # better than a mysterious no-op when `codexMaterializeMemories=true`.
246
- if [ "${REMNIC_CODEX_MATERIALIZE:-1}" != "0" ]; then
247
- HOOK_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" || HOOK_DIR=""
248
-
249
- MATERIALIZE_BIN="${REMNIC_CODEX_MATERIALIZE_BIN:-}"
250
- if [ -z "$MATERIALIZE_BIN" ] && [ -n "$HOOK_DIR" ]; then
251
- # hooks/bin/session-end.sh → ../../bin/materialize.cjs lands at
252
- # packages/plugin-codex/bin/materialize.cjs.
253
- CANDIDATE_BIN="${HOOK_DIR}/../../bin/materialize.cjs"
254
- if [ -f "$CANDIDATE_BIN" ]; then
255
- MATERIALIZE_BIN="$(cd -P "$(dirname "$CANDIDATE_BIN")" 2>/dev/null && pwd)/$(basename "$CANDIDATE_BIN")" || MATERIALIZE_BIN=""
256
- fi
257
- fi
258
-
259
- REMNIC_REPO_ROOT="${REMNIC_REPO_ROOT:-}"
260
- if [ -z "$REMNIC_REPO_ROOT" ] && [ -n "$HOOK_DIR" ]; then
261
- CANDIDATE_ROOT="$(cd -P "${HOOK_DIR}/../../../.." 2>/dev/null && pwd)" || CANDIDATE_ROOT=""
262
- if [ -n "$CANDIDATE_ROOT" ] && [ -f "${CANDIDATE_ROOT}/scripts/codex-materialize.ts" ]; then
263
- REMNIC_REPO_ROOT="$CANDIDATE_ROOT"
264
- fi
265
- fi
266
-
267
- if [ -n "$MATERIALIZE_BIN" ] && [ -f "$MATERIALIZE_BIN" ]; then
268
- node "$MATERIALIZE_BIN" --reason session_end >> "$LOG" 2>&1 || \
269
- log "codex-materialize session_end failed (packaged bin=${MATERIALIZE_BIN})"
270
- elif [ -n "$REMNIC_REPO_ROOT" ] && [ -f "${REMNIC_REPO_ROOT}/scripts/codex-materialize.ts" ]; then
271
- (
272
- cd "$REMNIC_REPO_ROOT"
273
- npx --yes tsx scripts/codex-materialize.ts --reason session_end >> "$LOG" 2>&1 || \
274
- log "codex-materialize session_end failed (dev script)"
275
- )
276
- else
277
- log "codex-materialize skipped — could not resolve packaged bin or REMNIC_REPO_ROOT (hook_dir=${HOOK_DIR:-unset})"
278
- fi
279
- fi
280
-
281
- exit 0