@remnic/plugin-claude-code 9.3.687 → 9.3.689
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/.claude-plugin/plugin.json +1 -1
- package/hooks/bin/remnic-cc-hook.cjs +917 -0
- package/hooks/bin/remnic-cc-hook.ps1 +10 -0
- package/hooks/bin/remnic-cc-hook.sh +7 -0
- package/hooks/bin/remnic-cc-hook.test.cjs +581 -0
- package/hooks/hooks.json +15 -3
- package/package.json +1 -1
- package/hooks/bin/post-tool-observe.sh +0 -279
- package/hooks/bin/session-end.sh +0 -38
- package/hooks/bin/session-start.sh +0 -254
- package/hooks/bin/user-prompt-recall.sh +0 -113
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Remnic PostToolUse hook for Claude Code.
|
|
3
|
-
# Observes file edits (Write/Edit/MultiEdit) by sending transcript
|
|
4
|
-
# delta to the 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') [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 cc = tokens.find(t => t.connector === 'claude-code');
|
|
42
|
-
const oc = tokens.find(t => t.connector === 'openclaw');
|
|
43
|
-
let tok = (cc && cc.token) || (oc && oc.token) || '';
|
|
44
|
-
if (!tok) { tok = store['claude-code'] || store['openclaw'] || ''; }
|
|
45
|
-
process.stdout.write(tok);
|
|
46
|
-
" "$TOKEN_FILE" 2>/dev/null || echo "")"
|
|
47
|
-
[ -n "$REMNIC_TOKEN" ] && break
|
|
48
|
-
done
|
|
49
|
-
[ -z "$REMNIC_TOKEN" ] && REMNIC_TOKEN="${OPENCLAW_REMNIC_ACCESS_TOKEN:-${OPENCLAW_ENGRAM_ACCESS_TOKEN:-}}"
|
|
50
|
-
|
|
51
|
-
INPUT="$(cat)"
|
|
52
|
-
|
|
53
|
-
# Return immediately — never block the tool
|
|
54
|
-
echo '{"continue":true}'
|
|
55
|
-
|
|
56
|
-
[ -z "$REMNIC_TOKEN" ] && exit 0
|
|
57
|
-
|
|
58
|
-
SESSION_ID="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.session_id||'')" "$INPUT" 2>/dev/null || echo "")"
|
|
59
|
-
TRANSCRIPT_PATH="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.transcript_path||'')" "$INPUT" 2>/dev/null || echo "")"
|
|
60
|
-
CWD="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.cwd||'')" "$INPUT" 2>/dev/null || echo "")"
|
|
61
|
-
TOOL_NAME="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.tool_name||'')" "$INPUT" 2>/dev/null || echo "")"
|
|
62
|
-
PROJECT_NAME="$(basename "$CWD" 2>/dev/null || echo "unknown")"
|
|
63
|
-
|
|
64
|
-
case "$SESSION_ID" in
|
|
65
|
-
""|*[!A-Za-z0-9._-]*)
|
|
66
|
-
log "invalid session id: $SESSION_ID"
|
|
67
|
-
exit 0
|
|
68
|
-
;;
|
|
69
|
-
esac
|
|
70
|
-
{ [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; } && exit 0
|
|
71
|
-
|
|
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"
|
|
97
|
-
|
|
98
|
-
if [ ! -f "$CURSOR_FILE" ] && { [ -f "$LEGACY_CURSOR_FILE" ] || [ -d "$LEGACY_LOCK_DIR" ]; }; then
|
|
99
|
-
CURSOR_FILE="$LEGACY_CURSOR_FILE"
|
|
100
|
-
LOCK_DIR="$LEGACY_LOCK_DIR"
|
|
101
|
-
fi
|
|
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
|
-
|
|
190
|
-
(
|
|
191
|
-
# Acquire exclusive lock
|
|
192
|
-
ACQUIRED=0
|
|
193
|
-
for _i in $(seq 1 50); do
|
|
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
|
|
196
|
-
sleep 0.1
|
|
197
|
-
done
|
|
198
|
-
trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT INT TERM
|
|
199
|
-
[ "$ACQUIRED" -eq 0 ] && exit 0
|
|
200
|
-
|
|
201
|
-
migrate_tmp_cursor_file
|
|
202
|
-
|
|
203
|
-
LAST_COUNT=0
|
|
204
|
-
LAST_COUNT="$(read_cursor_file)" || exit 0
|
|
205
|
-
|
|
206
|
-
PAYLOAD="$(node -e "
|
|
207
|
-
const fs = require('fs');
|
|
208
|
-
const path = process.argv[1];
|
|
209
|
-
const sessionId = process.argv[2];
|
|
210
|
-
const lastCount = parseInt(process.argv[3], 10) || 0;
|
|
211
|
-
|
|
212
|
-
const lines = fs.readFileSync(path, 'utf8').split('\n').filter(Boolean);
|
|
213
|
-
const messages = [];
|
|
214
|
-
for (const line of lines) {
|
|
215
|
-
try {
|
|
216
|
-
const entry = JSON.parse(line);
|
|
217
|
-
if (entry.type !== 'user' && entry.type !== 'assistant') continue;
|
|
218
|
-
const msg = entry.message;
|
|
219
|
-
if (!msg || typeof msg !== 'object') continue;
|
|
220
|
-
const role = msg.role;
|
|
221
|
-
if (role !== 'user' && role !== 'assistant') continue;
|
|
222
|
-
let text = '';
|
|
223
|
-
if (typeof msg.content === 'string') text = msg.content.trim();
|
|
224
|
-
else if (Array.isArray(msg.content)) {
|
|
225
|
-
text = msg.content
|
|
226
|
-
.filter(b => b.type === 'text' && b.text)
|
|
227
|
-
.map(b => b.text.trim())
|
|
228
|
-
.join('\n').trim();
|
|
229
|
-
}
|
|
230
|
-
if (text) messages.push({ role, content: text });
|
|
231
|
-
} catch {}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const newMessages = messages.slice(lastCount);
|
|
235
|
-
if (!newMessages.length) {
|
|
236
|
-
process.stdout.write('CURSOR:' + messages.length);
|
|
237
|
-
} else {
|
|
238
|
-
process.stdout.write(JSON.stringify({
|
|
239
|
-
sessionKey: sessionId,
|
|
240
|
-
messages: newMessages,
|
|
241
|
-
__total__: messages.length
|
|
242
|
-
}));
|
|
243
|
-
}
|
|
244
|
-
" "$TRANSCRIPT_PATH" "$SESSION_ID" "$LAST_COUNT" 2>/dev/null)"
|
|
245
|
-
|
|
246
|
-
[ -z "$PAYLOAD" ] && { log "parse failed for $SESSION_ID"; exit 0; }
|
|
247
|
-
|
|
248
|
-
if echo "$PAYLOAD" | grep -q "^CURSOR:"; then
|
|
249
|
-
write_cursor_file "${PAYLOAD#CURSOR:}" || log "cursor write failed for $SESSION_ID"
|
|
250
|
-
exit 0
|
|
251
|
-
fi
|
|
252
|
-
|
|
253
|
-
TOTAL="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(String(d.__total__||0))" "$PAYLOAD" 2>/dev/null || echo 0)"
|
|
254
|
-
MSG_COUNT="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(String((d.messages||[]).length))" "$PAYLOAD" 2>/dev/null || echo "?")"
|
|
255
|
-
CLEAN="$(node -e "const d=JSON.parse(process.argv[1]); delete d.__total__; process.stdout.write(JSON.stringify(d))" "$PAYLOAD" 2>/dev/null)"
|
|
256
|
-
|
|
257
|
-
[ -z "$CLEAN" ] && exit 0
|
|
258
|
-
|
|
259
|
-
log "observing $MSG_COUNT new messages (cursor $LAST_COUNT->$TOTAL) project=$PROJECT_NAME tool=$TOOL_NAME"
|
|
260
|
-
|
|
261
|
-
RAW="$(curl -s -w "\n%{http_code}" --max-time 120 \
|
|
262
|
-
-X POST "$REMNIC_URL" \
|
|
263
|
-
-H "Authorization: Bearer ${REMNIC_TOKEN}" \
|
|
264
|
-
-H "Content-Type: application/json" \
|
|
265
|
-
-H "X-Engram-Client-Id: claude-code" \
|
|
266
|
-
-d "$CLEAN" 2>/dev/null)"
|
|
267
|
-
CURL_EXIT=$?
|
|
268
|
-
HTTP_STATUS="$(echo "$RAW" | tail -1)"
|
|
269
|
-
|
|
270
|
-
if [ $CURL_EXIT -eq 0 ] && [[ "$HTTP_STATUS" =~ ^2 ]]; then
|
|
271
|
-
log "observe OK for $SESSION_ID"
|
|
272
|
-
write_cursor_file "$TOTAL" || log "cursor write failed for $SESSION_ID"
|
|
273
|
-
else
|
|
274
|
-
log "observe failed (curl=$CURL_EXIT http=$HTTP_STATUS) — cursor not advanced"
|
|
275
|
-
fi
|
|
276
|
-
) >> "$LOG" 2>&1 &
|
|
277
|
-
|
|
278
|
-
disown $!
|
|
279
|
-
exit 0
|
package/hooks/bin/session-end.sh
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Remnic session cleanup for Claude Code.
|
|
3
|
-
# Removes private cursor and lock files for the session.
|
|
4
|
-
#
|
|
5
|
-
# NOTE: Claude Code does not support a Stop/SessionEnd hook event.
|
|
6
|
-
# This script is provided for manual cleanup or future hook support.
|
|
7
|
-
# Private state files live under ${XDG_STATE_HOME:-$HOME/.local/state}/remnic/hooks.
|
|
8
|
-
|
|
9
|
-
INPUT="$(cat)"
|
|
10
|
-
SESSION_ID="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.session_id||'')" "$INPUT" 2>/dev/null || echo "")"
|
|
11
|
-
|
|
12
|
-
echo '{"continue":true}'
|
|
13
|
-
|
|
14
|
-
case "$SESSION_ID" in
|
|
15
|
-
""|*[!A-Za-z0-9._-]*)
|
|
16
|
-
exit 0
|
|
17
|
-
;;
|
|
18
|
-
esac
|
|
19
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
exit 0
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Remnic SessionStart hook for Claude Code.
|
|
3
|
-
# Recalls project context and user preferences at session start.
|
|
4
|
-
# Tries auto mode (45s) then falls back to minimal mode (20s).
|
|
5
|
-
# Starts daemon if not running.
|
|
6
|
-
|
|
7
|
-
set -euo pipefail
|
|
8
|
-
|
|
9
|
-
ensure_migrated() {
|
|
10
|
-
if [ -f "${HOME}/.remnic/.migrated-from-engram" ]; then
|
|
11
|
-
return 0
|
|
12
|
-
fi
|
|
13
|
-
if [ ! -d "${HOME}/.engram" ] && [ ! -f "${HOME}/.config/engram/config.json" ]; then
|
|
14
|
-
return 0
|
|
15
|
-
fi
|
|
16
|
-
if command -v remnic >/dev/null 2>&1; then
|
|
17
|
-
remnic migrate >/dev/null 2>&1 || true
|
|
18
|
-
elif command -v engram >/dev/null 2>&1; then
|
|
19
|
-
engram migrate >/dev/null 2>&1 || true
|
|
20
|
-
fi
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
ensure_migrated
|
|
24
|
-
|
|
25
|
-
REMNIC_HOST="${REMNIC_HOST:-${ENGRAM_HOST:-127.0.0.1}}"
|
|
26
|
-
REMNIC_PORT="${REMNIC_PORT:-${ENGRAM_PORT:-4318}}"
|
|
27
|
-
REMNIC_URL="http://${REMNIC_HOST}:${REMNIC_PORT}/engram/v1/recall"
|
|
28
|
-
REMNIC_HEALTH_URL="http://${REMNIC_HOST}:${REMNIC_PORT}/engram/v1/health"
|
|
29
|
-
TOKEN_FILES=("${HOME}/.remnic/tokens.json" "${HOME}/.engram/tokens.json")
|
|
30
|
-
|
|
31
|
-
LOG="${HOME}/.remnic/logs/remnic-session-recall.log"
|
|
32
|
-
mkdir -p "$(dirname "$LOG")"
|
|
33
|
-
log() { echo "$(date '+%F %T') [session-start] $*" >> "$LOG"; }
|
|
34
|
-
|
|
35
|
-
# Read token from per-plugin token store
|
|
36
|
-
REMNIC_TOKEN=""
|
|
37
|
-
for TOKEN_FILE in "${TOKEN_FILES[@]}"; do
|
|
38
|
-
[ ! -f "$TOKEN_FILE" ] && continue
|
|
39
|
-
REMNIC_TOKEN="$(node -e "
|
|
40
|
-
const store = JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));
|
|
41
|
-
const tokens = store.tokens || [];
|
|
42
|
-
const cc = tokens.find(t => t.connector === 'claude-code');
|
|
43
|
-
const oc = tokens.find(t => t.connector === 'openclaw');
|
|
44
|
-
let tok = (cc && cc.token) || (oc && oc.token) || '';
|
|
45
|
-
if (!tok) { tok = store['claude-code'] || store['openclaw'] || ''; }
|
|
46
|
-
process.stdout.write(tok);
|
|
47
|
-
" "$TOKEN_FILE" 2>/dev/null || echo "")"
|
|
48
|
-
[ -n "$REMNIC_TOKEN" ] && break
|
|
49
|
-
done
|
|
50
|
-
|
|
51
|
-
# Fallback to env var
|
|
52
|
-
[ -z "$REMNIC_TOKEN" ] && REMNIC_TOKEN="${OPENCLAW_REMNIC_ACCESS_TOKEN:-${OPENCLAW_ENGRAM_ACCESS_TOKEN:-}}"
|
|
53
|
-
|
|
54
|
-
INPUT="$(cat)"
|
|
55
|
-
SESSION_ID="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.session_id||'')" "$INPUT" 2>/dev/null || echo "")"
|
|
56
|
-
CWD="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.cwd||'')" "$INPUT" 2>/dev/null || echo "")"
|
|
57
|
-
PROJECT_NAME="$(basename "$CWD" 2>/dev/null || echo "unknown")"
|
|
58
|
-
|
|
59
|
-
# Resolve git context for the session's cwd (issue #569 PR 5). Produces
|
|
60
|
-
# either a JSON object for the `codingContext` field, or an empty string
|
|
61
|
-
# when the cwd is not inside a git repo. All git calls are wrapped in &&
|
|
62
|
-
# so any failure silently drops back to no-context.
|
|
63
|
-
CODING_CONTEXT_JSON=""
|
|
64
|
-
if [ -n "$CWD" ] && [ -d "$CWD" ] && command -v git >/dev/null 2>&1; then
|
|
65
|
-
# `git` calls are short-timeout and local. Any failure → empty.
|
|
66
|
-
REMNIC_GIT_TOP="$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null || echo "")"
|
|
67
|
-
if [ -n "$REMNIC_GIT_TOP" ]; then
|
|
68
|
-
REMNIC_GIT_BRANCH="$(git -C "$REMNIC_GIT_TOP" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "HEAD")"
|
|
69
|
-
[ "$REMNIC_GIT_BRANCH" = "HEAD" ] && REMNIC_GIT_BRANCH=""
|
|
70
|
-
REMNIC_GIT_ORIGIN="$(git -C "$REMNIC_GIT_TOP" remote get-url origin 2>/dev/null || echo "")"
|
|
71
|
-
REMNIC_GIT_DEFAULT_BRANCH="$(git -C "$REMNIC_GIT_TOP" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed 's|^refs/remotes/origin/||' || echo "")"
|
|
72
|
-
CODING_CONTEXT_JSON="$(REMNIC_GIT_TOP="$REMNIC_GIT_TOP" REMNIC_GIT_BRANCH="$REMNIC_GIT_BRANCH" REMNIC_GIT_ORIGIN="$REMNIC_GIT_ORIGIN" REMNIC_GIT_DEFAULT_BRANCH="$REMNIC_GIT_DEFAULT_BRANCH" node -e "
|
|
73
|
-
// Mirror the pure logic from @remnic/core's resolveGitContext so the
|
|
74
|
-
// hook produces the same projectId without calling into the daemon
|
|
75
|
-
// first. FNV-1a 32-bit stable hash.
|
|
76
|
-
const rootPath = process.env.REMNIC_GIT_TOP || '';
|
|
77
|
-
const branch = process.env.REMNIC_GIT_BRANCH || null;
|
|
78
|
-
const origin = process.env.REMNIC_GIT_ORIGIN || '';
|
|
79
|
-
const defaultBranch = process.env.REMNIC_GIT_DEFAULT_BRANCH || null;
|
|
80
|
-
function stableHash(input) {
|
|
81
|
-
let hash = 0x811c9dc5;
|
|
82
|
-
for (let i = 0; i < input.length; i++) {
|
|
83
|
-
hash ^= input.charCodeAt(i);
|
|
84
|
-
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
85
|
-
}
|
|
86
|
-
return hash.toString(16).padStart(8, '0');
|
|
87
|
-
}
|
|
88
|
-
// Mirrors packages/remnic-core/src/coding/git-context.ts
|
|
89
|
-
// normalizeOriginUrl. Keep the two in sync so the hook-computed
|
|
90
|
-
// projectId matches what the daemon computes on the same origin.
|
|
91
|
-
function normalizeOriginUrl(raw) {
|
|
92
|
-
let u = (raw || '').trim();
|
|
93
|
-
if (!u) return '';
|
|
94
|
-
// Case-insensitive .git strip — matches the TS canonical form.
|
|
95
|
-
if (/\\.git\$/i.test(u)) u = u.slice(0, -4);
|
|
96
|
-
// Windows drive-letter: short-circuit scp parsing.
|
|
97
|
-
if (/^[A-Za-z]:[\\\\/]/.test(u)) return u.toLowerCase();
|
|
98
|
-
// Protocol form: handles ssh://, https://, file:///, bracketed
|
|
99
|
-
// IPv6 hosts, optional user, optional port, and empty host
|
|
100
|
-
// (file:///path).
|
|
101
|
-
const proto = /^[a-z][a-z0-9+.-]*:\\/\\/(?:[^@/]+@)?(\\[[^\\]]+\\]|[^/:]*)(?::(\\d+))?(\\/.*)?\$/i.exec(u);
|
|
102
|
-
if (proto) {
|
|
103
|
-
let host = proto[1] || '';
|
|
104
|
-
const wasBracketed = host.startsWith('[') && host.endsWith(']');
|
|
105
|
-
if (wasBracketed) host = host.slice(1, -1);
|
|
106
|
-
const port = proto[2];
|
|
107
|
-
const p = (proto[3] || '').replace(/^\\/+/, '');
|
|
108
|
-
const hostPort = port
|
|
109
|
-
? (wasBracketed ? '[' + host + ']:' + port : host + ':' + port)
|
|
110
|
-
: host;
|
|
111
|
-
const prefix = hostPort.length > 0 ? hostPort : 'localhost';
|
|
112
|
-
return (prefix + '/' + p).toLowerCase();
|
|
113
|
-
}
|
|
114
|
-
// scp form: [user@]host:path — user@ optional, bracketed IPv6 host
|
|
115
|
-
// supported. A matched path starting with // is a protocol-URL
|
|
116
|
-
// leftover and is rejected.
|
|
117
|
-
const scp = /^(?:([^@\\s\\/]+)@)?(\\[[^\\]]+\\]|[^:@\\s\\/]+):(.+)\$/.exec(u);
|
|
118
|
-
if (scp) {
|
|
119
|
-
let host = scp[2] || '';
|
|
120
|
-
if (host.startsWith('[') && host.endsWith(']')) host = host.slice(1, -1);
|
|
121
|
-
const p = scp[3] || '';
|
|
122
|
-
if (p.startsWith('//')) return u.toLowerCase();
|
|
123
|
-
return (host + '/' + p.replace(/^\\/+/, '')).toLowerCase();
|
|
124
|
-
}
|
|
125
|
-
return u.toLowerCase();
|
|
126
|
-
}
|
|
127
|
-
const normalized = normalizeOriginUrl(origin);
|
|
128
|
-
const projectId = normalized ? 'origin:' + stableHash(normalized) : 'root:' + stableHash(rootPath);
|
|
129
|
-
process.stdout.write(JSON.stringify({
|
|
130
|
-
projectId,
|
|
131
|
-
branch: branch || null,
|
|
132
|
-
rootPath,
|
|
133
|
-
defaultBranch: defaultBranch || null,
|
|
134
|
-
}));
|
|
135
|
-
" 2>/dev/null || echo "")"
|
|
136
|
-
fi
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
log "session=$SESSION_ID project=$PROJECT_NAME coding-context=${CODING_CONTEXT_JSON:+yes}"
|
|
140
|
-
|
|
141
|
-
# Health check — start daemon if not running
|
|
142
|
-
if ! curl -sf --max-time 2 "$REMNIC_HEALTH_URL" >/dev/null 2>&1; then
|
|
143
|
-
log "daemon not responding, attempting start..."
|
|
144
|
-
if command -v remnic >/dev/null 2>&1; then
|
|
145
|
-
remnic daemon start >/dev/null 2>&1 &
|
|
146
|
-
elif command -v engram >/dev/null 2>&1; then
|
|
147
|
-
engram daemon start >/dev/null 2>&1 &
|
|
148
|
-
fi
|
|
149
|
-
sleep 2
|
|
150
|
-
if ! curl -sf --max-time 2 "$REMNIC_HEALTH_URL" >/dev/null 2>&1; then
|
|
151
|
-
log "daemon still not responding after start attempt"
|
|
152
|
-
echo '{"continue":true,"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"[Remnic: daemon not running — start with: remnic daemon start]"}}'
|
|
153
|
-
exit 0
|
|
154
|
-
fi
|
|
155
|
-
fi
|
|
156
|
-
|
|
157
|
-
if [ -z "$REMNIC_TOKEN" ]; then
|
|
158
|
-
log "skipping: no token found"
|
|
159
|
-
echo '{"continue":true,"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"[Remnic: no auth token — run: remnic connectors install claude-code]"}}'
|
|
160
|
-
exit 0
|
|
161
|
-
fi
|
|
162
|
-
|
|
163
|
-
QUERY="Starting a new coding session in project: ${PROJECT_NAME}. Recall relevant memories, preferences, decisions, patterns, and context about this project and the user."
|
|
164
|
-
|
|
165
|
-
REQUEST_BODY="$(REMNIC_CODING_CONTEXT_JSON="$CODING_CONTEXT_JSON" node -e "
|
|
166
|
-
const body = {
|
|
167
|
-
query: process.argv[1],
|
|
168
|
-
sessionKey: process.argv[2],
|
|
169
|
-
topK: 12,
|
|
170
|
-
mode: 'auto',
|
|
171
|
-
};
|
|
172
|
-
const raw = process.env.REMNIC_CODING_CONTEXT_JSON || '';
|
|
173
|
-
if (raw) {
|
|
174
|
-
try { body.codingContext = JSON.parse(raw); } catch (_) {
|
|
175
|
-
// Context envelope was provided but failed to parse. Explicitly
|
|
176
|
-
// clear any previously-attached context for this session so a
|
|
177
|
-
// malformed envelope does not silently keep stale state.
|
|
178
|
-
body.codingContext = null;
|
|
179
|
-
}
|
|
180
|
-
} else {
|
|
181
|
-
// No git context resolvable for this cwd. Explicitly clear any
|
|
182
|
-
// previously-attached context so a session that moves out of a repo
|
|
183
|
-
// does not keep routing to the old project namespace.
|
|
184
|
-
body.codingContext = null;
|
|
185
|
-
}
|
|
186
|
-
process.stdout.write(JSON.stringify(body));
|
|
187
|
-
" "$QUERY" "$SESSION_ID" 2>/dev/null)"
|
|
188
|
-
|
|
189
|
-
[ -z "$REQUEST_BODY" ] && echo '{"continue":true}' && exit 0
|
|
190
|
-
|
|
191
|
-
log "attempting full recall (auto mode)..."
|
|
192
|
-
RAW="$(curl -s -w "\n%{http_code}" --max-time 45 \
|
|
193
|
-
-X POST "$REMNIC_URL" \
|
|
194
|
-
-H "Authorization: Bearer ${REMNIC_TOKEN}" \
|
|
195
|
-
-H "Content-Type: application/json" \
|
|
196
|
-
-H "X-Engram-Client-Id: claude-code" \
|
|
197
|
-
-d "$REQUEST_BODY" 2>/dev/null)"
|
|
198
|
-
CURL_EXIT=$?
|
|
199
|
-
HTTP_STATUS="$(echo "$RAW" | tail -1)"
|
|
200
|
-
RESPONSE="$(echo "$RAW" | sed '$d')"
|
|
201
|
-
|
|
202
|
-
if [ $CURL_EXIT -ne 0 ] || ! [[ "$HTTP_STATUS" =~ ^2 ]] || [ -z "$RESPONSE" ]; then
|
|
203
|
-
log "full recall failed (curl=$CURL_EXIT http=$HTTP_STATUS) — falling back to minimal"
|
|
204
|
-
MINIMAL_BODY="$(REMNIC_CODING_CONTEXT_JSON="$CODING_CONTEXT_JSON" node -e "
|
|
205
|
-
const body = {
|
|
206
|
-
query: process.argv[1],
|
|
207
|
-
sessionKey: process.argv[2],
|
|
208
|
-
topK: 8,
|
|
209
|
-
mode: 'minimal',
|
|
210
|
-
};
|
|
211
|
-
const raw = process.env.REMNIC_CODING_CONTEXT_JSON || '';
|
|
212
|
-
if (raw) {
|
|
213
|
-
try { body.codingContext = JSON.parse(raw); } catch (_) { /* ignore */ }
|
|
214
|
-
}
|
|
215
|
-
process.stdout.write(JSON.stringify(body));
|
|
216
|
-
" "$QUERY" "$SESSION_ID" 2>/dev/null)"
|
|
217
|
-
RAW="$(curl -s -w "\n%{http_code}" --max-time 20 \
|
|
218
|
-
-X POST "$REMNIC_URL" \
|
|
219
|
-
-H "Authorization: Bearer ${REMNIC_TOKEN}" \
|
|
220
|
-
-H "Content-Type: application/json" \
|
|
221
|
-
-H "X-Engram-Client-Id: claude-code" \
|
|
222
|
-
-d "${MINIMAL_BODY:-$REQUEST_BODY}" 2>/dev/null)"
|
|
223
|
-
CURL_EXIT=$?
|
|
224
|
-
HTTP_STATUS="$(echo "$RAW" | tail -1)"
|
|
225
|
-
RESPONSE="$(echo "$RAW" | sed '$d')"
|
|
226
|
-
[[ "$CURL_EXIT" -eq 0 && "$HTTP_STATUS" =~ ^2 ]] && log "minimal recall succeeded" || { log "minimal recall also failed"; CURL_EXIT=1; }
|
|
227
|
-
fi
|
|
228
|
-
|
|
229
|
-
if [ $CURL_EXIT -eq 0 ] && [[ "$HTTP_STATUS" =~ ^2 ]] && [ -n "$RESPONSE" ]; then
|
|
230
|
-
CONTEXT="$(node -e "
|
|
231
|
-
const d = JSON.parse(process.argv[1]);
|
|
232
|
-
const ctx = d.context || '';
|
|
233
|
-
const count = d.count || 0;
|
|
234
|
-
const mode = d.mode || '';
|
|
235
|
-
if (ctx) {
|
|
236
|
-
const label = '[Remnic Memory Recall — ' + count + ' memories' + (mode ? ', ' + mode + ' mode' : '') + ']';
|
|
237
|
-
process.stdout.write(label + '\n\n' + ctx);
|
|
238
|
-
} else {
|
|
239
|
-
process.stdout.write('[Remnic: no relevant memories found for this session]');
|
|
240
|
-
}
|
|
241
|
-
" "$RESPONSE" 2>/dev/null || echo "[Remnic: recall parse error]")"
|
|
242
|
-
log "recall complete: $(echo "$CONTEXT" | head -1)"
|
|
243
|
-
else
|
|
244
|
-
CONTEXT="[Remnic: server unreachable — continuing without memory recall]"
|
|
245
|
-
log "$CONTEXT"
|
|
246
|
-
fi
|
|
247
|
-
|
|
248
|
-
node -e "
|
|
249
|
-
const context = process.argv[1];
|
|
250
|
-
process.stdout.write(JSON.stringify({
|
|
251
|
-
continue: true,
|
|
252
|
-
hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: context }
|
|
253
|
-
}));
|
|
254
|
-
" "$CONTEXT"
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Remnic UserPromptSubmit hook for Claude Code.
|
|
3
|
-
# Recalls per-prompt context using the user's message as query.
|
|
4
|
-
# Skips short prompts (<4 words). Minimal mode, 20s timeout.
|
|
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/recall"
|
|
27
|
-
|
|
28
|
-
LOG="${HOME}/.remnic/logs/remnic-user-prompt-recall.log"
|
|
29
|
-
mkdir -p "$(dirname "$LOG")"
|
|
30
|
-
log() { echo "$(date '+%F %T') [user-prompt] $*" >> "$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 cc = tokens.find(t => t.connector === 'claude-code');
|
|
42
|
-
const oc = tokens.find(t => t.connector === 'openclaw');
|
|
43
|
-
let tok = (cc && cc.token) || (oc && oc.token) || '';
|
|
44
|
-
if (!tok) { tok = store['claude-code'] || store['openclaw'] || ''; }
|
|
45
|
-
process.stdout.write(tok);
|
|
46
|
-
" "$TOKEN_FILE" 2>/dev/null || echo "")"
|
|
47
|
-
[ -n "$REMNIC_TOKEN" ] && break
|
|
48
|
-
done
|
|
49
|
-
[ -z "$REMNIC_TOKEN" ] && REMNIC_TOKEN="${OPENCLAW_REMNIC_ACCESS_TOKEN:-${OPENCLAW_ENGRAM_ACCESS_TOKEN:-}}"
|
|
50
|
-
|
|
51
|
-
INPUT="$(cat)"
|
|
52
|
-
|
|
53
|
-
if [ -z "$REMNIC_TOKEN" ]; then
|
|
54
|
-
echo '{"continue":true}'
|
|
55
|
-
exit 0
|
|
56
|
-
fi
|
|
57
|
-
|
|
58
|
-
SESSION_ID="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.session_id||'')" "$INPUT" 2>/dev/null || echo "")"
|
|
59
|
-
PROMPT="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.prompt||'')" "$INPUT" 2>/dev/null || echo "")"
|
|
60
|
-
|
|
61
|
-
# Skip very short prompts
|
|
62
|
-
WORD_COUNT="$(echo "$PROMPT" | wc -w | tr -d ' ')"
|
|
63
|
-
if [ "$WORD_COUNT" -lt 4 ]; then
|
|
64
|
-
echo '{"continue":true}'
|
|
65
|
-
exit 0
|
|
66
|
-
fi
|
|
67
|
-
|
|
68
|
-
log "session=$SESSION_ID words=$WORD_COUNT"
|
|
69
|
-
|
|
70
|
-
REQUEST_BODY="$(node -e "process.stdout.write(JSON.stringify({
|
|
71
|
-
query: process.argv[1],
|
|
72
|
-
sessionKey: process.argv[2],
|
|
73
|
-
topK: 8,
|
|
74
|
-
mode: 'minimal'
|
|
75
|
-
}))" "$PROMPT" "$SESSION_ID" 2>/dev/null)"
|
|
76
|
-
|
|
77
|
-
[ -z "$REQUEST_BODY" ] && echo '{"continue":true}' && exit 0
|
|
78
|
-
|
|
79
|
-
RAW="$(curl -s -w "\n%{http_code}" --max-time 20 \
|
|
80
|
-
-X POST "$REMNIC_URL" \
|
|
81
|
-
-H "Authorization: Bearer ${REMNIC_TOKEN}" \
|
|
82
|
-
-H "Content-Type: application/json" \
|
|
83
|
-
-H "X-Engram-Client-Id: claude-code" \
|
|
84
|
-
-d "$REQUEST_BODY" 2>/dev/null)"
|
|
85
|
-
CURL_EXIT=$?
|
|
86
|
-
HTTP_STATUS="$(echo "$RAW" | tail -1)"
|
|
87
|
-
RESPONSE="$(echo "$RAW" | sed '$d')"
|
|
88
|
-
|
|
89
|
-
if [ $CURL_EXIT -ne 0 ] || ! [[ "$HTTP_STATUS" =~ ^2 ]] || [ -z "$RESPONSE" ]; then
|
|
90
|
-
log "recall failed (curl=$CURL_EXIT http=$HTTP_STATUS)"
|
|
91
|
-
echo '{"continue":true}'
|
|
92
|
-
exit 0
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
node -e "
|
|
96
|
-
const d = JSON.parse(process.argv[1]);
|
|
97
|
-
const ctx = d.context || '';
|
|
98
|
-
const count = d.count || 0;
|
|
99
|
-
if (!ctx || count === 0) {
|
|
100
|
-
process.stdout.write(JSON.stringify({continue: true}));
|
|
101
|
-
} else {
|
|
102
|
-
process.stdout.write(JSON.stringify({
|
|
103
|
-
continue: true,
|
|
104
|
-
hookSpecificOutput: {
|
|
105
|
-
hookEventName: 'UserPromptSubmit',
|
|
106
|
-
additionalContext: '<remnic-memory count=\"' + count + '\">\n' + ctx + '\n</remnic-memory>'
|
|
107
|
-
}
|
|
108
|
-
}));
|
|
109
|
-
}
|
|
110
|
-
" "$RESPONSE" 2>/dev/null || echo '{"continue":true}'
|
|
111
|
-
|
|
112
|
-
COUNT="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(String(d.count||0))" "$RESPONSE" 2>/dev/null || echo "?")"
|
|
113
|
-
log "done: ${COUNT} memories injected"
|