@remnic/plugin-claude-code 9.3.687 → 9.3.688

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,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
@@ -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"