@remnic/plugin-codex 9.3.613 → 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.
- package/README.md +2 -2
- package/hooks/bin/remnic-codex-hook.cjs +969 -0
- package/hooks/bin/remnic-codex-hook.ps1 +10 -0
- package/hooks/bin/remnic-codex-hook.sh +7 -0
- package/hooks/bin/remnic-codex-hook.test.cjs +512 -0
- package/hooks/hooks.json +8 -4
- package/package.json +2 -2
- package/hooks/bin/post-tool-observe.sh +0 -280
- package/hooks/bin/session-end.sh +0 -281
- package/hooks/bin/session-start.sh +0 -257
- package/hooks/bin/user-prompt-recall.sh +0 -114
|
@@ -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
|
package/hooks/bin/session-end.sh
DELETED
|
@@ -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
|