openclaw-langcache 1.0.0 → 1.1.0
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 +6 -4
- package/claude-skills/langcache/SKILL.md +173 -0
- package/claude-skills/langcache/examples/agent-integration.py +453 -0
- package/claude-skills/langcache/examples/basic-caching.sh +56 -0
- package/claude-skills/langcache/references/api-reference.md +260 -0
- package/claude-skills/langcache/references/best-practices.md +215 -0
- package/claude-skills/langcache/scripts/langcache.sh +528 -0
- package/package.json +14 -8
- package/scripts/postinstall.js +116 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# langcache.sh - CLI wrapper for Redis LangCache REST API
|
|
4
|
+
# Enforces default caching policy with hard blocks
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# langcache.sh search <prompt> [--threshold <0.0-1.0>] [--attr <key=value>] [--strategy <exact,semantic>]
|
|
8
|
+
# langcache.sh store <prompt> <response> [--attr <key=value>]...
|
|
9
|
+
# langcache.sh delete --id <entry-id>
|
|
10
|
+
# langcache.sh delete --attr <key=value>
|
|
11
|
+
# langcache.sh flush
|
|
12
|
+
# langcache.sh check <text> # Check if text would be blocked
|
|
13
|
+
#
|
|
14
|
+
# Environment variables:
|
|
15
|
+
# LANGCACHE_HOST - LangCache API host (required)
|
|
16
|
+
# LANGCACHE_CACHE_ID - Cache ID (required)
|
|
17
|
+
# LANGCACHE_API_KEY - API key (required)
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
# Load secrets if available
|
|
22
|
+
SECRETS_FILE="${HOME}/.openclaw/secrets.env"
|
|
23
|
+
if [[ -f "$SECRETS_FILE" ]]; then
|
|
24
|
+
# shellcheck source=/dev/null
|
|
25
|
+
source "$SECRETS_FILE"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# =============================================================================
|
|
29
|
+
# HARD BLOCK PATTERNS - These NEVER get cached
|
|
30
|
+
# =============================================================================
|
|
31
|
+
|
|
32
|
+
# Temporal patterns (time-sensitive)
|
|
33
|
+
TEMPORAL_PATTERNS=(
|
|
34
|
+
'\b(today|tomorrow|tonight|yesterday)\b'
|
|
35
|
+
'\b(this|next|last)[[:space:]]+(week|month|year|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b'
|
|
36
|
+
'\bin[[:space:]]+[0-9]+[[:space:]]+(minutes?|hours?|days?|weeks?)\b'
|
|
37
|
+
'\b(deadline|eta|appointment|scheduled?)\b'
|
|
38
|
+
'\b(right[[:space:]]+now|at[[:space:]]+[0-9]{1,2}(:[0-9]{2})?[[:space:]]*(am|pm)?)\b'
|
|
39
|
+
'\bmeeting[[:space:]]+at\b'
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Credential patterns (security risk)
|
|
43
|
+
CREDENTIAL_PATTERNS=(
|
|
44
|
+
'\b(api[_-]?key|api[_-]?secret|access[_-]?token)\b'
|
|
45
|
+
'\b(password|passwd|pwd)[[:space:]]*[:=]'
|
|
46
|
+
'\b(secret[_-]?key|private[_-]?key)\b'
|
|
47
|
+
'\b(otp|2fa|totp|authenticator)[[:space:]]*(code|token)?\b'
|
|
48
|
+
'\bbearer[[:space:]]+[a-zA-Z0-9_-]+'
|
|
49
|
+
'\b(sk|pk)[_-][a-zA-Z0-9]{20,}\b'
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Identifier patterns (PII)
|
|
53
|
+
IDENTIFIER_PATTERNS=(
|
|
54
|
+
'\b[0-9]{10,15}\b'
|
|
55
|
+
'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b'
|
|
56
|
+
'\b(order|account|message|chat|user|customer)[_-]?id[[:space:]]*[:=]?[[:space:]]*[a-zA-Z0-9]+'
|
|
57
|
+
'\b[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b'
|
|
58
|
+
'@[a-zA-Z0-9_]{1,15}\b'
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Personal context patterns (privacy)
|
|
62
|
+
PERSONAL_PATTERNS=(
|
|
63
|
+
'\bmy[[:space:]]+(wife|husband|partner|girlfriend|boyfriend|spouse)\b'
|
|
64
|
+
'\bmy[[:space:]]+(mom|dad|mother|father|brother|sister|son|daughter|child|kid)\b'
|
|
65
|
+
'\bmy[[:space:]]+(friend|colleague|coworker|boss|manager)[[:space:]]+[A-Za-z]+'
|
|
66
|
+
'\b(said[[:space:]]+to[[:space:]]+me|told[[:space:]]+me|asked[[:space:]]+me|between[[:space:]]+us)\b'
|
|
67
|
+
'\b(private|confidential|secret)[[:space:]]+(conversation|chat|message)\b'
|
|
68
|
+
'\bin[[:space:]]+(our|my)[[:space:]]+(chat|conversation|thread|group)\b'
|
|
69
|
+
'\b(he|she|they)[[:space:]]+(said|told|asked|mentioned)\b'
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Check if text matches any block pattern
|
|
73
|
+
# Returns: 0 if blocked, 1 if allowed
|
|
74
|
+
# Output: block reason if blocked
|
|
75
|
+
check_blocks() {
|
|
76
|
+
local text="$1"
|
|
77
|
+
local text_lower
|
|
78
|
+
text_lower=$(echo "$text" | tr '[:upper:]' '[:lower:]')
|
|
79
|
+
|
|
80
|
+
# Check temporal patterns
|
|
81
|
+
for pattern in "${TEMPORAL_PATTERNS[@]}"; do
|
|
82
|
+
if echo "$text_lower" | grep -qiE "$pattern"; then
|
|
83
|
+
echo "temporal_info"
|
|
84
|
+
return 0
|
|
85
|
+
fi
|
|
86
|
+
done
|
|
87
|
+
|
|
88
|
+
# Check credential patterns
|
|
89
|
+
for pattern in "${CREDENTIAL_PATTERNS[@]}"; do
|
|
90
|
+
if echo "$text_lower" | grep -qiE "$pattern"; then
|
|
91
|
+
echo "credentials"
|
|
92
|
+
return 0
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
|
|
96
|
+
# Check identifier patterns
|
|
97
|
+
for pattern in "${IDENTIFIER_PATTERNS[@]}"; do
|
|
98
|
+
if echo "$text_lower" | grep -qiE "$pattern"; then
|
|
99
|
+
echo "identifiers"
|
|
100
|
+
return 0
|
|
101
|
+
fi
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
# Check personal patterns
|
|
105
|
+
for pattern in "${PERSONAL_PATTERNS[@]}"; do
|
|
106
|
+
if echo "$text_lower" | grep -qiE "$pattern"; then
|
|
107
|
+
echo "personal_context"
|
|
108
|
+
return 0
|
|
109
|
+
fi
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
return 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Validate required environment variables
|
|
116
|
+
check_env() {
|
|
117
|
+
local missing=()
|
|
118
|
+
[[ -z "${LANGCACHE_HOST:-}" ]] && missing+=("LANGCACHE_HOST")
|
|
119
|
+
[[ -z "${LANGCACHE_CACHE_ID:-}" ]] && missing+=("LANGCACHE_CACHE_ID")
|
|
120
|
+
[[ -z "${LANGCACHE_API_KEY:-}" ]] && missing+=("LANGCACHE_API_KEY")
|
|
121
|
+
|
|
122
|
+
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
123
|
+
echo "Error: Missing required environment variables: ${missing[*]}" >&2
|
|
124
|
+
echo "Set them in ~/.openclaw/secrets.env or export them directly." >&2
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Base URL for API calls
|
|
130
|
+
base_url() {
|
|
131
|
+
echo "https://${LANGCACHE_HOST}/v1/caches/${LANGCACHE_CACHE_ID}"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Make authenticated API request
|
|
135
|
+
api_request() {
|
|
136
|
+
local method="$1"
|
|
137
|
+
local endpoint="$2"
|
|
138
|
+
local data="${3:-}"
|
|
139
|
+
|
|
140
|
+
local url
|
|
141
|
+
url="$(base_url)${endpoint}"
|
|
142
|
+
|
|
143
|
+
local args=(
|
|
144
|
+
-s
|
|
145
|
+
-X "$method"
|
|
146
|
+
-H "Accept: application/json"
|
|
147
|
+
-H "Content-Type: application/json"
|
|
148
|
+
-H "Authorization: Bearer ${LANGCACHE_API_KEY}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if [[ -n "$data" ]]; then
|
|
152
|
+
args+=(-d "$data")
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
curl "${args[@]}" "$url"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Parse attributes from --attr key=value arguments
|
|
159
|
+
parse_attributes() {
|
|
160
|
+
local attrs=()
|
|
161
|
+
while [[ $# -gt 0 ]]; do
|
|
162
|
+
case "$1" in
|
|
163
|
+
--attr)
|
|
164
|
+
shift
|
|
165
|
+
attrs+=("$1")
|
|
166
|
+
shift
|
|
167
|
+
;;
|
|
168
|
+
*)
|
|
169
|
+
shift
|
|
170
|
+
;;
|
|
171
|
+
esac
|
|
172
|
+
done
|
|
173
|
+
|
|
174
|
+
if [[ ${#attrs[@]} -eq 0 ]]; then
|
|
175
|
+
echo "{}"
|
|
176
|
+
return
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
local json="{"
|
|
180
|
+
local first=true
|
|
181
|
+
for attr in "${attrs[@]}"; do
|
|
182
|
+
local key="${attr%%=*}"
|
|
183
|
+
local value="${attr#*=}"
|
|
184
|
+
if [[ "$first" == "true" ]]; then
|
|
185
|
+
first=false
|
|
186
|
+
else
|
|
187
|
+
json+=","
|
|
188
|
+
fi
|
|
189
|
+
json+="\"${key}\":\"${value}\""
|
|
190
|
+
done
|
|
191
|
+
json+="}"
|
|
192
|
+
echo "$json"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Check command - test if text would be blocked
|
|
196
|
+
cmd_check() {
|
|
197
|
+
local text="$1"
|
|
198
|
+
|
|
199
|
+
if [[ -z "$text" ]]; then
|
|
200
|
+
echo "Error: Check requires text to analyze" >&2
|
|
201
|
+
exit 1
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
local block_reason
|
|
205
|
+
if block_reason=$(check_blocks "$text"); then
|
|
206
|
+
echo "BLOCKED: $block_reason"
|
|
207
|
+
echo "This content will NOT be cached."
|
|
208
|
+
return 1
|
|
209
|
+
else
|
|
210
|
+
echo "ALLOWED: Content can be cached"
|
|
211
|
+
return 0
|
|
212
|
+
fi
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Search for cached response
|
|
216
|
+
cmd_search() {
|
|
217
|
+
local prompt=""
|
|
218
|
+
local threshold=""
|
|
219
|
+
local strategy=""
|
|
220
|
+
local attrs_args=()
|
|
221
|
+
|
|
222
|
+
while [[ $# -gt 0 ]]; do
|
|
223
|
+
case "$1" in
|
|
224
|
+
--threshold)
|
|
225
|
+
shift
|
|
226
|
+
threshold="$1"
|
|
227
|
+
shift
|
|
228
|
+
;;
|
|
229
|
+
--strategy)
|
|
230
|
+
shift
|
|
231
|
+
strategy="$1"
|
|
232
|
+
shift
|
|
233
|
+
;;
|
|
234
|
+
--attr)
|
|
235
|
+
attrs_args+=(--attr)
|
|
236
|
+
shift
|
|
237
|
+
attrs_args+=("$1")
|
|
238
|
+
shift
|
|
239
|
+
;;
|
|
240
|
+
*)
|
|
241
|
+
if [[ -z "$prompt" ]]; then
|
|
242
|
+
prompt="$1"
|
|
243
|
+
fi
|
|
244
|
+
shift
|
|
245
|
+
;;
|
|
246
|
+
esac
|
|
247
|
+
done
|
|
248
|
+
|
|
249
|
+
if [[ -z "$prompt" ]]; then
|
|
250
|
+
echo "Error: Search requires a prompt" >&2
|
|
251
|
+
echo "Usage: langcache.sh search <prompt> [--threshold <0.0-1.0>] [--attr <key=value>]" >&2
|
|
252
|
+
exit 1
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
# Check for hard blocks before searching
|
|
256
|
+
local block_reason
|
|
257
|
+
if block_reason=$(check_blocks "$prompt"); then
|
|
258
|
+
echo "{\"hit\": false, \"blocked\": true, \"reason\": \"$block_reason\"}"
|
|
259
|
+
echo "Warning: Prompt contains blocked content ($block_reason), skipping cache" >&2
|
|
260
|
+
return 0
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# Build JSON payload
|
|
264
|
+
local json
|
|
265
|
+
json=$(jq -n \
|
|
266
|
+
--arg prompt "$prompt" \
|
|
267
|
+
'{prompt: $prompt}')
|
|
268
|
+
|
|
269
|
+
# Add similarity threshold if specified
|
|
270
|
+
if [[ -n "$threshold" ]]; then
|
|
271
|
+
json=$(echo "$json" | jq --arg t "$threshold" '. + {similarityThreshold: ($t | tonumber)}')
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# Add search strategies if specified
|
|
275
|
+
if [[ -n "$strategy" ]]; then
|
|
276
|
+
IFS=',' read -ra strategies <<< "$strategy"
|
|
277
|
+
json=$(echo "$json" | jq --argjson s "$(printf '%s\n' "${strategies[@]}" | jq -R . | jq -s .)" '. + {searchStrategies: $s}')
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# Add attributes if specified
|
|
281
|
+
if [[ ${#attrs_args[@]} -gt 0 ]]; then
|
|
282
|
+
local attrs
|
|
283
|
+
attrs=$(parse_attributes "${attrs_args[@]}")
|
|
284
|
+
json=$(echo "$json" | jq --argjson a "$attrs" '. + {attributes: $a}')
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
api_request POST "/entries/search" "$json"
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# Store new cache entry
|
|
291
|
+
cmd_store() {
|
|
292
|
+
local prompt=""
|
|
293
|
+
local response=""
|
|
294
|
+
local attrs_args=()
|
|
295
|
+
local force=false
|
|
296
|
+
|
|
297
|
+
while [[ $# -gt 0 ]]; do
|
|
298
|
+
case "$1" in
|
|
299
|
+
--attr)
|
|
300
|
+
attrs_args+=(--attr)
|
|
301
|
+
shift
|
|
302
|
+
attrs_args+=("$1")
|
|
303
|
+
shift
|
|
304
|
+
;;
|
|
305
|
+
--force)
|
|
306
|
+
force=true
|
|
307
|
+
shift
|
|
308
|
+
;;
|
|
309
|
+
*)
|
|
310
|
+
if [[ -z "$prompt" ]]; then
|
|
311
|
+
prompt="$1"
|
|
312
|
+
elif [[ -z "$response" ]]; then
|
|
313
|
+
response="$1"
|
|
314
|
+
fi
|
|
315
|
+
shift
|
|
316
|
+
;;
|
|
317
|
+
esac
|
|
318
|
+
done
|
|
319
|
+
|
|
320
|
+
if [[ -z "$prompt" || -z "$response" ]]; then
|
|
321
|
+
echo "Error: Store requires prompt and response" >&2
|
|
322
|
+
echo "Usage: langcache.sh store <prompt> <response> [--attr <key=value>]..." >&2
|
|
323
|
+
exit 1
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
# Check for hard blocks in prompt
|
|
327
|
+
local block_reason
|
|
328
|
+
if block_reason=$(check_blocks "$prompt"); then
|
|
329
|
+
echo "Error: Prompt contains blocked content ($block_reason)" >&2
|
|
330
|
+
echo "This content cannot be cached. Blocked categories:" >&2
|
|
331
|
+
echo " - temporal_info: time-sensitive data (today, tomorrow, deadlines)" >&2
|
|
332
|
+
echo " - credentials: API keys, passwords, tokens" >&2
|
|
333
|
+
echo " - identifiers: emails, phone numbers, account IDs" >&2
|
|
334
|
+
echo " - personal_context: private conversations, relationships" >&2
|
|
335
|
+
if [[ "$force" != "true" ]]; then
|
|
336
|
+
exit 1
|
|
337
|
+
fi
|
|
338
|
+
echo "Warning: --force flag used, storing anyway (not recommended)" >&2
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
# Check for hard blocks in response
|
|
342
|
+
if block_reason=$(check_blocks "$response"); then
|
|
343
|
+
echo "Error: Response contains blocked content ($block_reason)" >&2
|
|
344
|
+
if [[ "$force" != "true" ]]; then
|
|
345
|
+
exit 1
|
|
346
|
+
fi
|
|
347
|
+
echo "Warning: --force flag used, storing anyway (not recommended)" >&2
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
# Build JSON payload
|
|
351
|
+
local json
|
|
352
|
+
json=$(jq -n \
|
|
353
|
+
--arg prompt "$prompt" \
|
|
354
|
+
--arg response "$response" \
|
|
355
|
+
'{prompt: $prompt, response: $response}')
|
|
356
|
+
|
|
357
|
+
# Add attributes if specified
|
|
358
|
+
if [[ ${#attrs_args[@]} -gt 0 ]]; then
|
|
359
|
+
local attrs
|
|
360
|
+
attrs=$(parse_attributes "${attrs_args[@]}")
|
|
361
|
+
json=$(echo "$json" | jq --argjson a "$attrs" '. + {attributes: $a}')
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
api_request POST "/entries" "$json"
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
# Delete cache entries
|
|
368
|
+
cmd_delete() {
|
|
369
|
+
local entry_id=""
|
|
370
|
+
local attrs_args=()
|
|
371
|
+
|
|
372
|
+
while [[ $# -gt 0 ]]; do
|
|
373
|
+
case "$1" in
|
|
374
|
+
--id)
|
|
375
|
+
shift
|
|
376
|
+
entry_id="$1"
|
|
377
|
+
shift
|
|
378
|
+
;;
|
|
379
|
+
--attr)
|
|
380
|
+
attrs_args+=(--attr)
|
|
381
|
+
shift
|
|
382
|
+
attrs_args+=("$1")
|
|
383
|
+
shift
|
|
384
|
+
;;
|
|
385
|
+
*)
|
|
386
|
+
shift
|
|
387
|
+
;;
|
|
388
|
+
esac
|
|
389
|
+
done
|
|
390
|
+
|
|
391
|
+
if [[ -n "$entry_id" ]]; then
|
|
392
|
+
# Delete by ID
|
|
393
|
+
api_request DELETE "/entries/${entry_id}"
|
|
394
|
+
elif [[ ${#attrs_args[@]} -gt 0 ]]; then
|
|
395
|
+
# Delete by attributes
|
|
396
|
+
local attrs
|
|
397
|
+
attrs=$(parse_attributes "${attrs_args[@]}")
|
|
398
|
+
local json
|
|
399
|
+
json=$(jq -n --argjson a "$attrs" '{attributes: $a}')
|
|
400
|
+
api_request DELETE "/entries" "$json"
|
|
401
|
+
else
|
|
402
|
+
echo "Error: Delete requires --id or --attr" >&2
|
|
403
|
+
echo "Usage: langcache.sh delete --id <entry-id>" >&2
|
|
404
|
+
echo " langcache.sh delete --attr <key=value>" >&2
|
|
405
|
+
exit 1
|
|
406
|
+
fi
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# Flush entire cache
|
|
410
|
+
cmd_flush() {
|
|
411
|
+
echo "Warning: This will delete ALL cache entries. Proceed? (y/N)" >&2
|
|
412
|
+
read -r confirm
|
|
413
|
+
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
|
414
|
+
api_request POST "/flush"
|
|
415
|
+
echo "Cache flushed." >&2
|
|
416
|
+
else
|
|
417
|
+
echo "Aborted." >&2
|
|
418
|
+
exit 1
|
|
419
|
+
fi
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# Force flush without confirmation (for scripts)
|
|
423
|
+
cmd_flush_force() {
|
|
424
|
+
api_request POST "/flush"
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
# Show usage
|
|
428
|
+
usage() {
|
|
429
|
+
cat <<EOF
|
|
430
|
+
langcache.sh - CLI wrapper for Redis LangCache REST API
|
|
431
|
+
Enforces default caching policy with hard blocks.
|
|
432
|
+
|
|
433
|
+
Commands:
|
|
434
|
+
search <prompt> Search for cached response
|
|
435
|
+
--threshold Similarity threshold (0.0-1.0, default: uses cache default)
|
|
436
|
+
--strategy Search strategy: exact, semantic, or both (comma-separated)
|
|
437
|
+
--attr key=value Filter by attribute (can be repeated)
|
|
438
|
+
|
|
439
|
+
store <prompt> <response> Store new cache entry
|
|
440
|
+
--attr key=value Add attribute (can be repeated)
|
|
441
|
+
--force Store even if blocked (not recommended)
|
|
442
|
+
|
|
443
|
+
delete Delete cache entries
|
|
444
|
+
--id <entry-id> Delete by entry ID
|
|
445
|
+
--attr key=value Delete by attribute match
|
|
446
|
+
|
|
447
|
+
check <text> Check if text would be blocked by policy
|
|
448
|
+
|
|
449
|
+
flush Clear entire cache (with confirmation)
|
|
450
|
+
flush-force Clear entire cache (no confirmation, for scripts)
|
|
451
|
+
|
|
452
|
+
Caching Policy (enforced automatically):
|
|
453
|
+
CACHEABLE:
|
|
454
|
+
- Factual Q&A, definitions, documentation
|
|
455
|
+
- Command explanations
|
|
456
|
+
- Reusable reply templates (polite no, follow-up, intro)
|
|
457
|
+
- Style transforms (warmer, shorter, firmer)
|
|
458
|
+
|
|
459
|
+
BLOCKED (hard blocks):
|
|
460
|
+
- Temporal info: today, tomorrow, deadlines, ETAs, appointments
|
|
461
|
+
- Credentials: API keys, tokens, passwords, OTP/2FA
|
|
462
|
+
- Identifiers: emails, phone numbers, addresses, account IDs
|
|
463
|
+
- Personal context: relationships, private conversations
|
|
464
|
+
|
|
465
|
+
Environment:
|
|
466
|
+
LANGCACHE_HOST LangCache API host
|
|
467
|
+
LANGCACHE_CACHE_ID Cache ID
|
|
468
|
+
LANGCACHE_API_KEY API key
|
|
469
|
+
|
|
470
|
+
Set these in ~/.openclaw/secrets.env or export directly.
|
|
471
|
+
|
|
472
|
+
Examples:
|
|
473
|
+
langcache.sh search "What is Redis?"
|
|
474
|
+
langcache.sh search "What is Redis?" --threshold 0.9
|
|
475
|
+
langcache.sh store "What is Redis?" "Redis is an in-memory data store..."
|
|
476
|
+
langcache.sh store "prompt" "response" --attr model=gpt-5 --attr category=factual
|
|
477
|
+
langcache.sh check "What's on my calendar today?" # Will show: BLOCKED: temporal_info
|
|
478
|
+
langcache.sh delete --id abc123
|
|
479
|
+
langcache.sh delete --attr user=123
|
|
480
|
+
EOF
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
# Main
|
|
484
|
+
main() {
|
|
485
|
+
if [[ $# -eq 0 ]]; then
|
|
486
|
+
usage
|
|
487
|
+
exit 0
|
|
488
|
+
fi
|
|
489
|
+
|
|
490
|
+
local cmd="$1"
|
|
491
|
+
shift
|
|
492
|
+
|
|
493
|
+
case "$cmd" in
|
|
494
|
+
search)
|
|
495
|
+
check_env
|
|
496
|
+
cmd_search "$@"
|
|
497
|
+
;;
|
|
498
|
+
store)
|
|
499
|
+
check_env
|
|
500
|
+
cmd_store "$@"
|
|
501
|
+
;;
|
|
502
|
+
delete)
|
|
503
|
+
check_env
|
|
504
|
+
cmd_delete "$@"
|
|
505
|
+
;;
|
|
506
|
+
check)
|
|
507
|
+
cmd_check "$@"
|
|
508
|
+
;;
|
|
509
|
+
flush)
|
|
510
|
+
check_env
|
|
511
|
+
cmd_flush "$@"
|
|
512
|
+
;;
|
|
513
|
+
flush-force)
|
|
514
|
+
check_env
|
|
515
|
+
cmd_flush_force "$@"
|
|
516
|
+
;;
|
|
517
|
+
help|--help|-h)
|
|
518
|
+
usage
|
|
519
|
+
;;
|
|
520
|
+
*)
|
|
521
|
+
echo "Unknown command: $cmd" >&2
|
|
522
|
+
usage
|
|
523
|
+
exit 1
|
|
524
|
+
;;
|
|
525
|
+
esac
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
main "$@"
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-langcache",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Semantic caching skill for OpenClaw using Redis LangCache",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Semantic caching skill for OpenClaw and Claude Code using Redis LangCache",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
7
|
+
"claude-code",
|
|
7
8
|
"skill",
|
|
8
9
|
"langcache",
|
|
9
10
|
"redis",
|
|
@@ -12,13 +13,13 @@
|
|
|
12
13
|
"ai",
|
|
13
14
|
"cache"
|
|
14
15
|
],
|
|
15
|
-
"homepage": "https://github.com/openclaw
|
|
16
|
+
"homepage": "https://github.com/manvinder01/openclaw-langcache#readme",
|
|
16
17
|
"bugs": {
|
|
17
|
-
"url": "https://github.com/openclaw
|
|
18
|
+
"url": "https://github.com/manvinder01/openclaw-langcache/issues"
|
|
18
19
|
},
|
|
19
20
|
"repository": {
|
|
20
21
|
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/openclaw
|
|
22
|
+
"url": "git+https://github.com/manvinder01/openclaw-langcache.git"
|
|
22
23
|
},
|
|
23
24
|
"license": "MIT",
|
|
24
25
|
"author": {
|
|
@@ -26,6 +27,8 @@
|
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"skills/",
|
|
30
|
+
"claude-skills/",
|
|
31
|
+
"scripts/",
|
|
29
32
|
"README.md",
|
|
30
33
|
"LICENSE"
|
|
31
34
|
],
|
|
@@ -37,9 +40,12 @@
|
|
|
37
40
|
},
|
|
38
41
|
"openclaw": {
|
|
39
42
|
"type": "skill",
|
|
40
|
-
"skills": [
|
|
41
|
-
"langcache"
|
|
42
|
-
],
|
|
43
|
+
"skills": ["langcache"],
|
|
43
44
|
"installPath": "skills/"
|
|
45
|
+
},
|
|
46
|
+
"claude-code": {
|
|
47
|
+
"type": "skill",
|
|
48
|
+
"skills": ["langcache"],
|
|
49
|
+
"installPath": "claude-skills/"
|
|
44
50
|
}
|
|
45
51
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall script for openclaw-langcache
|
|
4
|
+
* Installs the skill to both OpenClaw and Claude Code workspaces
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
const SKILL_NAME = 'langcache';
|
|
12
|
+
|
|
13
|
+
// Get installation paths
|
|
14
|
+
function getInstallPaths() {
|
|
15
|
+
const home = os.homedir();
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
openclaw: path.join(
|
|
19
|
+
process.env.OPENCLAW_HOME || path.join(home, '.openclaw'),
|
|
20
|
+
'workspace', 'skills'
|
|
21
|
+
),
|
|
22
|
+
claude: path.join(
|
|
23
|
+
process.env.CLAUDE_HOME || path.join(home, '.claude'),
|
|
24
|
+
'skills'
|
|
25
|
+
)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Recursively copy directory
|
|
30
|
+
function copyDir(src, dest) {
|
|
31
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
32
|
+
|
|
33
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
34
|
+
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const srcPath = path.join(src, entry.name);
|
|
37
|
+
const destPath = path.join(dest, entry.name);
|
|
38
|
+
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
copyDir(srcPath, destPath);
|
|
41
|
+
} else {
|
|
42
|
+
fs.copyFileSync(srcPath, destPath);
|
|
43
|
+
|
|
44
|
+
// Make shell scripts and python files executable
|
|
45
|
+
if (entry.name.endsWith('.sh') || entry.name.endsWith('.py')) {
|
|
46
|
+
fs.chmodSync(destPath, 0o755);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function installSkill(name, srcDir, destDir, skillSrcPath) {
|
|
53
|
+
const destPath = path.join(destDir, SKILL_NAME);
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(skillSrcPath)) {
|
|
56
|
+
console.log(` ⚠ ${name}: Source not found, skipping`);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create skills directory if needed
|
|
61
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
// Check if already exists
|
|
64
|
+
if (fs.existsSync(destPath)) {
|
|
65
|
+
console.log(` ⚠ ${name}: Already exists at ${destPath}`);
|
|
66
|
+
console.log(` To update: rm -rf ${destPath} && npm install openclaw-langcache`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Copy skill
|
|
71
|
+
copyDir(skillSrcPath, destPath);
|
|
72
|
+
console.log(` ✓ ${name}: Installed to ${destPath}`);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function main() {
|
|
77
|
+
console.log('\n📦 Installing langcache skill...\n');
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const paths = getInstallPaths();
|
|
81
|
+
const packageRoot = path.dirname(__dirname);
|
|
82
|
+
|
|
83
|
+
let installed = 0;
|
|
84
|
+
|
|
85
|
+
// Install OpenClaw skill
|
|
86
|
+
const openclawSrc = path.join(packageRoot, 'skills', SKILL_NAME);
|
|
87
|
+
if (installSkill('OpenClaw', 'skills', paths.openclaw, openclawSrc)) {
|
|
88
|
+
installed++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Install Claude Code skill
|
|
92
|
+
const claudeSrc = path.join(packageRoot, 'claude-skills', SKILL_NAME);
|
|
93
|
+
if (installSkill('Claude Code', 'claude-skills', paths.claude, claudeSrc)) {
|
|
94
|
+
installed++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (installed > 0) {
|
|
98
|
+
console.log('\n✅ Installation complete!\n');
|
|
99
|
+
console.log('Next steps:');
|
|
100
|
+
console.log('1. Set your Redis LangCache credentials:');
|
|
101
|
+
console.log(' export LANGCACHE_HOST=your-instance.redis.cloud');
|
|
102
|
+
console.log(' export LANGCACHE_CACHE_ID=your-cache-id');
|
|
103
|
+
console.log(' export LANGCACHE_API_KEY=your-api-key');
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log('2. The skill auto-activates when you mention "semantic caching"');
|
|
106
|
+
console.log(' or invoke manually with /langcache');
|
|
107
|
+
console.log('');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.warn(`⚠ Warning: ${err.message}`);
|
|
112
|
+
console.warn('You can manually copy skills from node_modules/openclaw-langcache/');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
main();
|