@hitechclaw/clawspark 2.0.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/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +378 -0
- package/clawspark +2715 -0
- package/configs/models.yaml +108 -0
- package/configs/skill-packs.yaml +44 -0
- package/configs/skills.yaml +37 -0
- package/install.sh +387 -0
- package/lib/common.sh +249 -0
- package/lib/detect-hardware.sh +156 -0
- package/lib/diagnose.sh +636 -0
- package/lib/render-diagram.sh +47 -0
- package/lib/sandbox-commands.sh +415 -0
- package/lib/secure.sh +244 -0
- package/lib/select-model.sh +442 -0
- package/lib/setup-browser.sh +138 -0
- package/lib/setup-dashboard.sh +228 -0
- package/lib/setup-inference.sh +128 -0
- package/lib/setup-mcp.sh +142 -0
- package/lib/setup-messaging.sh +242 -0
- package/lib/setup-models.sh +121 -0
- package/lib/setup-openclaw.sh +808 -0
- package/lib/setup-sandbox.sh +188 -0
- package/lib/setup-skills.sh +113 -0
- package/lib/setup-systemd.sh +224 -0
- package/lib/setup-tailscale.sh +188 -0
- package/lib/setup-voice.sh +101 -0
- package/lib/skill-audit.sh +449 -0
- package/lib/verify.sh +177 -0
- package/package.json +57 -0
- package/scripts/release.sh +133 -0
- package/uninstall.sh +161 -0
- package/v2/README.md +50 -0
- package/v2/configs/providers.yaml +79 -0
- package/v2/configs/skills.yaml +36 -0
- package/v2/install.sh +116 -0
- package/v2/lib/common.sh +285 -0
- package/v2/lib/detect-hardware.sh +119 -0
- package/v2/lib/select-runtime.sh +273 -0
- package/v2/lib/setup-extras.sh +95 -0
- package/v2/lib/setup-openclaw.sh +187 -0
- package/v2/lib/setup-provider.sh +131 -0
- package/v2/lib/verify.sh +133 -0
- package/web/index.html +1835 -0
- package/web/install.sh +387 -0
- package/web/logo-hero.svg +11 -0
- package/web/logo-icon.svg +12 -0
- package/web/logo.svg +17 -0
- package/web/vercel.json +8 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SKILLS_DIR="${HOME}/.openclaw/skills"
|
|
5
|
+
CLAWSPARK_DIR="${CLAWSPARK_DIR:-${HOME}/.clawspark}"
|
|
6
|
+
HASH_FILE="${CLAWSPARK_DIR}/skill-hashes.json"
|
|
7
|
+
AUDIT_REPORT="${CLAWSPARK_DIR}/audit-report.txt"
|
|
8
|
+
|
|
9
|
+
ALLOWLIST=(
|
|
10
|
+
"local-whisper"
|
|
11
|
+
"self-improvement"
|
|
12
|
+
"memory-setup"
|
|
13
|
+
"whatsapp-voice-chat-integration-open-source"
|
|
14
|
+
"deep-research-pro"
|
|
15
|
+
"agent-browser"
|
|
16
|
+
"second-brain"
|
|
17
|
+
"proactive-agent"
|
|
18
|
+
"ddg-web-search"
|
|
19
|
+
"local-web-search-skill"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_NETWORK_PATTERNS=(
|
|
23
|
+
'fetch('
|
|
24
|
+
'axios'
|
|
25
|
+
'http\.request'
|
|
26
|
+
'net\.connect'
|
|
27
|
+
'XMLHttpRequest'
|
|
28
|
+
'WebSocket('
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_CREDENTIAL_PATTERNS=(
|
|
32
|
+
'~/\.ssh'
|
|
33
|
+
'\$HOME/\.ssh'
|
|
34
|
+
'~/\.aws'
|
|
35
|
+
'\$HOME/\.aws'
|
|
36
|
+
'~/\.env'
|
|
37
|
+
'/etc/passwd'
|
|
38
|
+
'/etc/shadow'
|
|
39
|
+
'process\.env\.'
|
|
40
|
+
'os\.environ'
|
|
41
|
+
'keychain'
|
|
42
|
+
'credential'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
_OBFUSCATION_PATTERNS=(
|
|
46
|
+
'eval('
|
|
47
|
+
'Function('
|
|
48
|
+
'atob('
|
|
49
|
+
'Buffer\.from.*base64'
|
|
50
|
+
'exec('
|
|
51
|
+
'__import__'
|
|
52
|
+
'compile('
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
_EXFILTRATION_PATTERNS=(
|
|
56
|
+
'FormData'
|
|
57
|
+
'encodeURIComponent.*token'
|
|
58
|
+
'btoa('
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
_FILESYSTEM_PATTERNS=(
|
|
62
|
+
'\.\./\.\.'
|
|
63
|
+
'\.\.\\/\.\.\\'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
_PROCESS_PATTERNS=(
|
|
67
|
+
'child_process'
|
|
68
|
+
'subprocess'
|
|
69
|
+
'os\.system'
|
|
70
|
+
'os\.popen'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
_is_allowlisted() {
|
|
74
|
+
local name="$1"
|
|
75
|
+
for allowed in "${ALLOWLIST[@]}"; do
|
|
76
|
+
if [[ "${name}" == "${allowed}" ]]; then
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_compute_skill_hash() {
|
|
84
|
+
local skill_dir="$1"
|
|
85
|
+
if check_command shasum; then
|
|
86
|
+
find "${skill_dir}" -type f \( -name '*.js' -o -name '*.ts' -o -name '*.py' -o -name '*.sh' -o -name '*.json' \) \
|
|
87
|
+
-exec shasum -a 256 {} \; 2>/dev/null | sort | shasum -a 256 | cut -d' ' -f1
|
|
88
|
+
elif check_command sha256sum; then
|
|
89
|
+
find "${skill_dir}" -type f \( -name '*.js' -o -name '*.ts' -o -name '*.py' -o -name '*.sh' -o -name '*.json' \) \
|
|
90
|
+
-exec sha256sum {} \; 2>/dev/null | sort | sha256sum | cut -d' ' -f1
|
|
91
|
+
else
|
|
92
|
+
echo "no-hash-tool"
|
|
93
|
+
fi
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_load_hashes() {
|
|
97
|
+
if [[ -f "${HASH_FILE}" ]]; then
|
|
98
|
+
cat "${HASH_FILE}"
|
|
99
|
+
else
|
|
100
|
+
echo "{}"
|
|
101
|
+
fi
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_save_hashes() {
|
|
105
|
+
local json="$1"
|
|
106
|
+
echo "${json}" > "${HASH_FILE}"
|
|
107
|
+
chmod 600 "${HASH_FILE}"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_escape_sed() {
|
|
111
|
+
printf '%s' "$1" | sed 's/[&/\]/\\&/g'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_get_stored_hash() {
|
|
115
|
+
local json="$1"
|
|
116
|
+
local skill="$2"
|
|
117
|
+
if check_command jq; then
|
|
118
|
+
echo "${json}" | jq -r --arg k "${skill}" '.[$k] // empty' 2>/dev/null || echo ""
|
|
119
|
+
else
|
|
120
|
+
local escaped
|
|
121
|
+
escaped=$(_escape_sed "${skill}")
|
|
122
|
+
echo "${json}" | grep -o "\"${escaped}\":\"[a-f0-9]*\"" | cut -d'"' -f4 || echo ""
|
|
123
|
+
fi
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_set_hash_entry() {
|
|
127
|
+
local json="$1"
|
|
128
|
+
local skill="$2"
|
|
129
|
+
local hash="$3"
|
|
130
|
+
|
|
131
|
+
if check_command jq; then
|
|
132
|
+
echo "${json}" | jq --arg k "${skill}" --arg v "${hash}" '.[$k] = $v' 2>/dev/null
|
|
133
|
+
return
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
local escaped_skill
|
|
137
|
+
escaped_skill=$(_escape_sed "${skill}")
|
|
138
|
+
|
|
139
|
+
if [[ "${json}" == "{}" ]]; then
|
|
140
|
+
echo "{\"${skill}\":\"${hash}\"}"
|
|
141
|
+
return
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
if echo "${json}" | grep -q "\"${escaped_skill}\":"; then
|
|
145
|
+
echo "${json}" | sed "s/\"${escaped_skill}\":\"[a-f0-9]*\"/\"${escaped_skill}\":\"${hash}\"/"
|
|
146
|
+
else
|
|
147
|
+
echo "${json}" | sed "s/}$/,\"${escaped_skill}\":\"${hash}\"}/"
|
|
148
|
+
fi
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_scan_patterns() {
|
|
152
|
+
local skill_dir="$1"
|
|
153
|
+
local -a findings=()
|
|
154
|
+
|
|
155
|
+
local source_file_list
|
|
156
|
+
source_file_list=$(mktemp)
|
|
157
|
+
find "${skill_dir}" -type f \( -name '*.js' -o -name '*.ts' -o -name '*.py' -o -name '*.sh' \) -print0 2>/dev/null > "${source_file_list}" || true
|
|
158
|
+
|
|
159
|
+
if [[ ! -s "${source_file_list}" ]]; then
|
|
160
|
+
rm -f "${source_file_list}"
|
|
161
|
+
echo ""
|
|
162
|
+
return
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
local -a _categories=("network" "credential" "obfuscation" "exfiltration" "path_traversal" "process_spawn")
|
|
166
|
+
local -a _arrays=("_NETWORK_PATTERNS" "_CREDENTIAL_PATTERNS" "_OBFUSCATION_PATTERNS" "_EXFILTRATION_PATTERNS" "_FILESYSTEM_PATTERNS" "_PROCESS_PATTERNS")
|
|
167
|
+
|
|
168
|
+
local i pattern category arr_name
|
|
169
|
+
for i in $(seq 0 $(( ${#_categories[@]} - 1 ))); do
|
|
170
|
+
category="${_categories[$i]}"
|
|
171
|
+
arr_name="${_arrays[$i]}"
|
|
172
|
+
eval "local -a _pats=(\"\${${arr_name}[@]}\")"
|
|
173
|
+
for pattern in "${_pats[@]}"; do
|
|
174
|
+
if xargs -0 grep -l "${pattern}" < "${source_file_list}" 2>/dev/null | head -1 | grep -q .; then
|
|
175
|
+
findings+=("${category}:${pattern}")
|
|
176
|
+
fi
|
|
177
|
+
done
|
|
178
|
+
done
|
|
179
|
+
|
|
180
|
+
rm -f "${source_file_list}"
|
|
181
|
+
|
|
182
|
+
local IFS="|"
|
|
183
|
+
echo "${findings[*]:-}"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_dir_size_human() {
|
|
187
|
+
du -sh "$1" 2>/dev/null | cut -f1 | tr -d ' '
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
audit_skills() {
|
|
191
|
+
log_info "Running skill security audit..."
|
|
192
|
+
hr
|
|
193
|
+
|
|
194
|
+
mkdir -p "${CLAWSPARK_DIR}"
|
|
195
|
+
|
|
196
|
+
if [[ ! -d "${SKILLS_DIR}" ]]; then
|
|
197
|
+
log_warn "Skills directory not found: ${SKILLS_DIR}"
|
|
198
|
+
log_info "No skills installed. Nothing to audit."
|
|
199
|
+
return 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
local -a skill_dirs=()
|
|
203
|
+
for d in "${SKILLS_DIR}"/*/; do
|
|
204
|
+
[[ -d "${d}" ]] && skill_dirs+=("${d}")
|
|
205
|
+
done
|
|
206
|
+
|
|
207
|
+
if [[ ${#skill_dirs[@]} -eq 0 ]]; then
|
|
208
|
+
log_info "No skills installed. Nothing to audit."
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
local total_pass=0
|
|
213
|
+
local total_warn=0
|
|
214
|
+
local total_fail=0
|
|
215
|
+
local timestamp
|
|
216
|
+
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
217
|
+
|
|
218
|
+
local hashes
|
|
219
|
+
hashes=$(_load_hashes)
|
|
220
|
+
|
|
221
|
+
local col_name=30
|
|
222
|
+
local col_status=8
|
|
223
|
+
local separator
|
|
224
|
+
separator=$(printf '%90s' '' | tr ' ' '─')
|
|
225
|
+
local header_line
|
|
226
|
+
header_line=$(printf '%-*s %-*s %s' ${col_name} "SKILL NAME" ${col_status} "STATUS" "FINDINGS")
|
|
227
|
+
|
|
228
|
+
printf '\n %s%s%s\n' "${BOLD}" "${header_line}" "${RESET}"
|
|
229
|
+
printf ' %s%s%s\n' "${BLUE}" "${separator}" "${RESET}"
|
|
230
|
+
|
|
231
|
+
local report_lines=()
|
|
232
|
+
report_lines+=("ClawSpark Skill Audit Report")
|
|
233
|
+
report_lines+=("Timestamp: ${timestamp}")
|
|
234
|
+
report_lines+=("Skills directory: ${SKILLS_DIR}")
|
|
235
|
+
report_lines+=("${separator}")
|
|
236
|
+
report_lines+=("${header_line}")
|
|
237
|
+
report_lines+=("${separator}")
|
|
238
|
+
|
|
239
|
+
for skill_path in "${skill_dirs[@]}"; do
|
|
240
|
+
local skill_name
|
|
241
|
+
skill_name=$(basename "${skill_path}")
|
|
242
|
+
local size
|
|
243
|
+
size=$(_dir_size_human "${skill_path}")
|
|
244
|
+
|
|
245
|
+
local -a issues=()
|
|
246
|
+
local status="PASS"
|
|
247
|
+
local status_color="${GREEN}"
|
|
248
|
+
|
|
249
|
+
if [[ ! -f "${skill_path}/SKILL.md" ]] && [[ ! -f "${skill_path}/skill.md" ]]; then
|
|
250
|
+
issues+=("missing SKILL.md")
|
|
251
|
+
if [[ "${status}" != "FAIL" ]]; then
|
|
252
|
+
status="WARN"
|
|
253
|
+
status_color="${YELLOW}"
|
|
254
|
+
fi
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
if ! _is_allowlisted "${skill_name}"; then
|
|
258
|
+
issues+=("unverified (not in allowlist)")
|
|
259
|
+
if [[ "${status}" != "FAIL" ]]; then
|
|
260
|
+
status="WARN"
|
|
261
|
+
status_color="${YELLOW}"
|
|
262
|
+
fi
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
local scan_result
|
|
266
|
+
scan_result=$(_scan_patterns "${skill_path}")
|
|
267
|
+
|
|
268
|
+
if [[ -n "${scan_result}" ]]; then
|
|
269
|
+
local IFS='|'
|
|
270
|
+
local -a pattern_hits=()
|
|
271
|
+
read -ra pattern_hits <<< "${scan_result}"
|
|
272
|
+
|
|
273
|
+
local has_critical=false
|
|
274
|
+
local finding
|
|
275
|
+
for finding in "${pattern_hits[@]}"; do
|
|
276
|
+
local category="${finding%%:*}"
|
|
277
|
+
local detail="${finding#*:}"
|
|
278
|
+
issues+=("${category}: ${detail}")
|
|
279
|
+
case "${category}" in
|
|
280
|
+
credential|exfiltration|obfuscation|process_spawn)
|
|
281
|
+
has_critical=true
|
|
282
|
+
;;
|
|
283
|
+
esac
|
|
284
|
+
done
|
|
285
|
+
|
|
286
|
+
if ${has_critical}; then
|
|
287
|
+
status="FAIL"
|
|
288
|
+
status_color="${RED}"
|
|
289
|
+
elif [[ "${status}" != "FAIL" ]]; then
|
|
290
|
+
status="WARN"
|
|
291
|
+
status_color="${YELLOW}"
|
|
292
|
+
fi
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
local current_hash
|
|
296
|
+
current_hash=$(_compute_skill_hash "${skill_path}")
|
|
297
|
+
local stored_hash
|
|
298
|
+
stored_hash=$(_get_stored_hash "${hashes}" "${skill_name}")
|
|
299
|
+
|
|
300
|
+
if [[ -n "${stored_hash}" && "${stored_hash}" != "${current_hash}" && "${stored_hash}" != "no-hash-tool" && "${current_hash}" != "no-hash-tool" ]]; then
|
|
301
|
+
issues+=("files changed since last audit")
|
|
302
|
+
if [[ "${status}" == "PASS" ]]; then
|
|
303
|
+
status="WARN"
|
|
304
|
+
status_color="${YELLOW}"
|
|
305
|
+
fi
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
hashes=$(_set_hash_entry "${hashes}" "${skill_name}" "${current_hash}")
|
|
309
|
+
|
|
310
|
+
local findings_str
|
|
311
|
+
if [[ ${#issues[@]} -eq 0 ]]; then
|
|
312
|
+
findings_str="clean (${size})"
|
|
313
|
+
else
|
|
314
|
+
local IFS="; "
|
|
315
|
+
findings_str="${issues[*]} (${size})"
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
printf ' %-*s %s%-*s%s %s\n' \
|
|
319
|
+
${col_name} "${skill_name}" \
|
|
320
|
+
"${status_color}" ${col_status} "${status}" "${RESET}" \
|
|
321
|
+
"${findings_str}"
|
|
322
|
+
|
|
323
|
+
report_lines+=("$(printf '%-*s %-*s %s' ${col_name} "${skill_name}" ${col_status} "${status}" "${findings_str}")")
|
|
324
|
+
|
|
325
|
+
case "${status}" in
|
|
326
|
+
PASS) total_pass=$(( total_pass + 1 )) ;;
|
|
327
|
+
WARN) total_warn=$(( total_warn + 1 )) ;;
|
|
328
|
+
FAIL) total_fail=$(( total_fail + 1 )) ;;
|
|
329
|
+
esac
|
|
330
|
+
done
|
|
331
|
+
|
|
332
|
+
_save_hashes "${hashes}"
|
|
333
|
+
|
|
334
|
+
printf ' %s%s%s\n\n' "${BLUE}" "${separator}" "${RESET}"
|
|
335
|
+
|
|
336
|
+
report_lines+=("${separator}")
|
|
337
|
+
report_lines+=("")
|
|
338
|
+
|
|
339
|
+
local summary
|
|
340
|
+
summary="Total: ${GREEN}${total_pass} PASS${RESET}, ${YELLOW}${total_warn} WARN${RESET}, ${RED}${total_fail} FAIL${RESET}"
|
|
341
|
+
printf ' %s\n\n' "${summary}"
|
|
342
|
+
|
|
343
|
+
local summary_plain="Total: ${total_pass} PASS, ${total_warn} WARN, ${total_fail} FAIL"
|
|
344
|
+
report_lines+=("${summary_plain}")
|
|
345
|
+
|
|
346
|
+
{
|
|
347
|
+
for line in "${report_lines[@]}"; do
|
|
348
|
+
echo "${line}"
|
|
349
|
+
done
|
|
350
|
+
} > "${AUDIT_REPORT}"
|
|
351
|
+
chmod 600 "${AUDIT_REPORT}"
|
|
352
|
+
|
|
353
|
+
if (( total_fail > 0 )); then
|
|
354
|
+
log_error "${total_fail} skill(s) flagged as FAIL — review immediately."
|
|
355
|
+
print_box \
|
|
356
|
+
"${RED}${BOLD}Security Alert${RESET}" \
|
|
357
|
+
"" \
|
|
358
|
+
"${total_fail} skill(s) contain suspicious patterns." \
|
|
359
|
+
"Run 'clawspark skills remove <name>' for any untrusted skill." \
|
|
360
|
+
"Report: ${AUDIT_REPORT}"
|
|
361
|
+
elif (( total_warn > 0 )); then
|
|
362
|
+
log_warn "${total_warn} skill(s) flagged as WARN — review recommended."
|
|
363
|
+
else
|
|
364
|
+
log_success "All ${total_pass} skill(s) passed security audit."
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
log_info "Audit report saved to ${AUDIT_REPORT}"
|
|
368
|
+
log_info "Hash database saved to ${HASH_FILE}"
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_quick_skill_check() {
|
|
372
|
+
local skill_path="$1"
|
|
373
|
+
local skill_name
|
|
374
|
+
skill_name=$(basename "${skill_path}")
|
|
375
|
+
|
|
376
|
+
if [[ ! -d "${skill_path}" ]]; then
|
|
377
|
+
log_error "Skill directory not found: ${skill_path}"
|
|
378
|
+
return 1
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
log_info "Quick security check: ${skill_name}"
|
|
382
|
+
|
|
383
|
+
local -a issues=()
|
|
384
|
+
local status="PASS"
|
|
385
|
+
|
|
386
|
+
if [[ ! -f "${skill_path}/SKILL.md" ]] && [[ ! -f "${skill_path}/skill.md" ]]; then
|
|
387
|
+
issues+=("missing SKILL.md")
|
|
388
|
+
status="WARN"
|
|
389
|
+
fi
|
|
390
|
+
|
|
391
|
+
if ! _is_allowlisted "${skill_name}"; then
|
|
392
|
+
issues+=("not in curated allowlist")
|
|
393
|
+
status="WARN"
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
local scan_result
|
|
397
|
+
scan_result=$(_scan_patterns "${skill_path}")
|
|
398
|
+
|
|
399
|
+
if [[ -n "${scan_result}" ]]; then
|
|
400
|
+
local IFS='|'
|
|
401
|
+
local -a pattern_hits=()
|
|
402
|
+
read -ra pattern_hits <<< "${scan_result}"
|
|
403
|
+
|
|
404
|
+
local finding
|
|
405
|
+
for finding in "${pattern_hits[@]}"; do
|
|
406
|
+
local category="${finding%%:*}"
|
|
407
|
+
local detail="${finding#*:}"
|
|
408
|
+
issues+=("${category}: ${detail}")
|
|
409
|
+
case "${category}" in
|
|
410
|
+
credential|exfiltration|obfuscation|process_spawn)
|
|
411
|
+
status="FAIL"
|
|
412
|
+
;;
|
|
413
|
+
*)
|
|
414
|
+
if [[ "${status}" != "FAIL" ]]; then
|
|
415
|
+
status="WARN"
|
|
416
|
+
fi
|
|
417
|
+
;;
|
|
418
|
+
esac
|
|
419
|
+
done
|
|
420
|
+
fi
|
|
421
|
+
|
|
422
|
+
case "${status}" in
|
|
423
|
+
PASS)
|
|
424
|
+
log_success "Skill '${skill_name}' passed pre-install check."
|
|
425
|
+
return 0
|
|
426
|
+
;;
|
|
427
|
+
WARN)
|
|
428
|
+
log_warn "Skill '${skill_name}' has warnings:"
|
|
429
|
+
for issue in "${issues[@]}"; do
|
|
430
|
+
printf ' %s⚠%s %s\n' "${YELLOW}" "${RESET}" "${issue}"
|
|
431
|
+
done
|
|
432
|
+
return 0
|
|
433
|
+
;;
|
|
434
|
+
FAIL)
|
|
435
|
+
log_error "Skill '${skill_name}' BLOCKED — suspicious patterns detected:"
|
|
436
|
+
for issue in "${issues[@]}"; do
|
|
437
|
+
printf ' %s✗%s %s\n' "${RED}" "${RESET}" "${issue}"
|
|
438
|
+
done
|
|
439
|
+
printf '\n'
|
|
440
|
+
print_box \
|
|
441
|
+
"${RED}${BOLD}Installation Blocked${RESET}" \
|
|
442
|
+
"" \
|
|
443
|
+
"Skill '${skill_name}' contains patterns associated with malware." \
|
|
444
|
+
"341 malicious skills were found on ClawHub (Atomic Stealer, data theft)." \
|
|
445
|
+
"Use --force to override this check at your own risk."
|
|
446
|
+
return 1
|
|
447
|
+
;;
|
|
448
|
+
esac
|
|
449
|
+
}
|
package/lib/verify.sh
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lib/verify.sh — Post-installation health checks and performance benchmark.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
verify_installation() {
|
|
6
|
+
log_info "Running verification checks..."
|
|
7
|
+
hr
|
|
8
|
+
|
|
9
|
+
local pass=0
|
|
10
|
+
local fail=0
|
|
11
|
+
|
|
12
|
+
# ── Helper to print check results ───────────────────────────────────────
|
|
13
|
+
_check_pass() {
|
|
14
|
+
printf ' %s✓%s %s\n' "${GREEN}" "${RESET}" "$1"
|
|
15
|
+
pass=$(( pass + 1 ))
|
|
16
|
+
}
|
|
17
|
+
_check_fail() {
|
|
18
|
+
printf ' %s✗%s %s\n' "${RED}" "${RESET}" "$1"
|
|
19
|
+
fail=$(( fail + 1 ))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
printf '\n'
|
|
23
|
+
|
|
24
|
+
# ── 1. Hardware ─────────────────────────────────────────────────────────
|
|
25
|
+
local ram_gb=$(( ${HW_TOTAL_RAM_MB:-0} / 1024 ))
|
|
26
|
+
local platform_label
|
|
27
|
+
case "${HW_PLATFORM:-generic}" in
|
|
28
|
+
dgx-spark) platform_label="DGX Spark (${ram_gb}GB unified memory)" ;;
|
|
29
|
+
jetson) platform_label="Jetson (${ram_gb}GB)" ;;
|
|
30
|
+
rtx) platform_label="RTX (${HW_GPU_VRAM_MB:-0}MB VRAM)" ;;
|
|
31
|
+
mac) platform_label="macOS Apple Silicon (${ram_gb}GB unified)" ;;
|
|
32
|
+
*) platform_label="Generic (${ram_gb}GB RAM)" ;;
|
|
33
|
+
esac
|
|
34
|
+
_check_pass "Hardware: ${platform_label}"
|
|
35
|
+
|
|
36
|
+
# ── 2. Model ────────────────────────────────────────────────────────────
|
|
37
|
+
_check_pass "Model: ${SELECTED_MODEL_NAME:-unknown} (${SELECTED_MODEL_ID:-unknown})"
|
|
38
|
+
|
|
39
|
+
# ── 3. Ollama ───────────────────────────────────────────────────────────
|
|
40
|
+
if curl -sf http://127.0.0.1:11434/ &>/dev/null; then
|
|
41
|
+
_check_pass "Inference: Ollama (http://127.0.0.1:11434)"
|
|
42
|
+
else
|
|
43
|
+
_check_fail "Inference: Ollama is not responding"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# ── 4. Model loaded ────────────────────────────────────────────────────
|
|
47
|
+
if ollama list 2>/dev/null | grep -q "${SELECTED_MODEL_ID}"; then
|
|
48
|
+
_check_pass "Model available in Ollama"
|
|
49
|
+
else
|
|
50
|
+
_check_fail "Model ${SELECTED_MODEL_ID} not found in Ollama"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# ── 5. OpenClaw ─────────────────────────────────────────────────────────
|
|
54
|
+
if check_command openclaw; then
|
|
55
|
+
local oc_ver
|
|
56
|
+
oc_ver=$(openclaw --version 2>/dev/null || echo "unknown")
|
|
57
|
+
local gw_status="gateway not running"
|
|
58
|
+
if check_command systemctl && systemctl is-active --quiet clawspark-gateway.service 2>/dev/null; then
|
|
59
|
+
gw_status="gateway running (systemd)"
|
|
60
|
+
elif [[ -f "${CLAWSPARK_DIR}/gateway.pid" ]]; then
|
|
61
|
+
local gw_pid
|
|
62
|
+
gw_pid=$(cat "${CLAWSPARK_DIR}/gateway.pid")
|
|
63
|
+
if [[ -n "${gw_pid}" ]] && kill -0 "${gw_pid}" 2>/dev/null; then
|
|
64
|
+
gw_status="gateway running"
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
_check_pass "OpenClaw: ${oc_ver} (${gw_status})"
|
|
68
|
+
else
|
|
69
|
+
_check_fail "OpenClaw: not installed"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# ── 6. Tools profile ──────────────────────────────────────────────────
|
|
73
|
+
local tools_profile
|
|
74
|
+
tools_profile=$(openclaw config get tools.profile 2>/dev/null || echo "unknown")
|
|
75
|
+
if [[ "${tools_profile}" == *"full"* ]]; then
|
|
76
|
+
_check_pass "Tools: full profile (web, exec, browser, filesystem)"
|
|
77
|
+
else
|
|
78
|
+
_check_fail "Tools: profile is '${tools_profile}' (should be 'full')"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# ── 7. Node host ──────────────────────────────────────────────────────
|
|
82
|
+
if check_command systemctl && systemctl is-active --quiet clawspark-nodehost.service 2>/dev/null; then
|
|
83
|
+
_check_pass "Node host: running (systemd)"
|
|
84
|
+
elif [[ -f "${CLAWSPARK_DIR}/node.pid" ]]; then
|
|
85
|
+
local node_pid
|
|
86
|
+
node_pid=$(cat "${CLAWSPARK_DIR}/node.pid")
|
|
87
|
+
if [[ -n "${node_pid}" ]] && kill -0 "${node_pid}" 2>/dev/null; then
|
|
88
|
+
_check_pass "Node host: running (PID ${node_pid})"
|
|
89
|
+
else
|
|
90
|
+
_check_fail "Node host: not running"
|
|
91
|
+
fi
|
|
92
|
+
else
|
|
93
|
+
_check_fail "Node host: not started"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# ── 8. Skills ──────────────────────────────────────────────────────────
|
|
97
|
+
local skill_count=0
|
|
98
|
+
if [[ -f "${CLAWSPARK_DIR}/skills.yaml" ]]; then
|
|
99
|
+
skill_count=$(grep -c '^ *- ' "${CLAWSPARK_DIR}/skills.yaml" 2>/dev/null || echo 0)
|
|
100
|
+
fi
|
|
101
|
+
_check_pass "Skills: ${skill_count} configured"
|
|
102
|
+
|
|
103
|
+
# ── 9. Voice ────────────────────────────────────────────────────────────
|
|
104
|
+
local whisper_model="${WHISPER_MODEL:-unknown}"
|
|
105
|
+
if [[ -f "${HOME}/.openclaw/skills/local-whisper/config.json" ]]; then
|
|
106
|
+
_check_pass "Voice: Whisper ${whisper_model} ready"
|
|
107
|
+
else
|
|
108
|
+
_check_fail "Voice: Whisper config not found"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# ── 10. Security ────────────────────────────────────────────────────────
|
|
112
|
+
if [[ -f "${CLAWSPARK_DIR}/token" ]]; then
|
|
113
|
+
_check_pass "Security: localhost-only, token auth"
|
|
114
|
+
else
|
|
115
|
+
_check_fail "Security: token not generated"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# ── 11. Messaging ───────────────────────────────────────────────────────
|
|
119
|
+
local msg_status="${MESSAGING_CHOICE:-skip}"
|
|
120
|
+
if [[ "${msg_status}" != "skip" ]]; then
|
|
121
|
+
_check_pass "Messaging: ${msg_status} configured"
|
|
122
|
+
else
|
|
123
|
+
_check_pass "Messaging: skipped"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# ── 12. Quick benchmark ────────────────────────────────────────────────
|
|
127
|
+
local toks_str="n/a"
|
|
128
|
+
if curl -sf http://127.0.0.1:11434/ &>/dev/null; then
|
|
129
|
+
toks_str=$(_run_benchmark)
|
|
130
|
+
fi
|
|
131
|
+
_check_pass "Performance: ${toks_str}"
|
|
132
|
+
|
|
133
|
+
# ── Summary ─────────────────────────────────────────────────────────────
|
|
134
|
+
printf '\n'
|
|
135
|
+
if (( fail == 0 )); then
|
|
136
|
+
log_success "All ${pass} checks passed."
|
|
137
|
+
else
|
|
138
|
+
log_warn "${pass} passed, ${fail} failed. Review the items marked with ✗ above."
|
|
139
|
+
fi
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# ── Benchmark ───────────────────────────────────────────────────────────────
|
|
143
|
+
# Sends a short prompt and measures tokens/second from the Ollama API.
|
|
144
|
+
_run_benchmark() {
|
|
145
|
+
local start_ms end_ms elapsed_ms
|
|
146
|
+
local response tok_count tps
|
|
147
|
+
|
|
148
|
+
start_ms=$(date +%s%3N 2>/dev/null || python3 -c 'import time; print(int(time.time()*1000))' 2>/dev/null || echo 0)
|
|
149
|
+
|
|
150
|
+
response=$(curl -sf --max-time 30 http://127.0.0.1:11434/api/generate \
|
|
151
|
+
-d "{\"model\":\"${SELECTED_MODEL_ID}\",\"prompt\":\"Count from 1 to 10.\",\"stream\":false,\"options\":{\"num_predict\":10}}" 2>/dev/null || echo "")
|
|
152
|
+
|
|
153
|
+
end_ms=$(date +%s%3N 2>/dev/null || python3 -c 'import time; print(int(time.time()*1000))' 2>/dev/null || echo 0)
|
|
154
|
+
|
|
155
|
+
if [[ -z "${response}" ]]; then
|
|
156
|
+
echo "benchmark skipped (API timeout)"
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# Try to extract eval_count and eval_duration from Ollama response
|
|
161
|
+
local eval_count eval_duration_ns
|
|
162
|
+
eval_count=$(echo "${response}" | grep -o '"eval_count":[0-9]*' | cut -d: -f2 || echo "")
|
|
163
|
+
eval_duration_ns=$(echo "${response}" | grep -o '"eval_duration":[0-9]*' | cut -d: -f2 || echo "")
|
|
164
|
+
|
|
165
|
+
if [[ -n "${eval_count}" && -n "${eval_duration_ns}" && "${eval_duration_ns}" -gt 0 ]]; then
|
|
166
|
+
# eval_duration is in nanoseconds
|
|
167
|
+
tps=$(awk "BEGIN {printf \"%.1f\", ${eval_count} / (${eval_duration_ns} / 1000000000)}")
|
|
168
|
+
echo "~${tps} tok/s"
|
|
169
|
+
else
|
|
170
|
+
elapsed_ms=$(( end_ms - start_ms ))
|
|
171
|
+
if (( elapsed_ms > 0 )); then
|
|
172
|
+
echo "~$(( 10 * 1000 / elapsed_ms )) tok/s (estimated)"
|
|
173
|
+
else
|
|
174
|
+
echo "benchmark unavailable"
|
|
175
|
+
fi
|
|
176
|
+
fi
|
|
177
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hitechclaw/clawspark",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "One-command OpenClaw installer and CLI for local, CPU-first, and API-backed AI agent deployments.",
|
|
5
|
+
"author": "hitechclaw",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/thanhan92-f1/clawspark#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/thanhan92-f1/clawspark.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/thanhan92-f1/clawspark/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"openclaw",
|
|
17
|
+
"ai-agent",
|
|
18
|
+
"cli",
|
|
19
|
+
"ollama",
|
|
20
|
+
"installer",
|
|
21
|
+
"local-ai",
|
|
22
|
+
"cpu-ai",
|
|
23
|
+
"api-gateway"
|
|
24
|
+
],
|
|
25
|
+
"bin": {
|
|
26
|
+
"clawspark": "clawspark"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"clawspark",
|
|
30
|
+
"install.sh",
|
|
31
|
+
"uninstall.sh",
|
|
32
|
+
"scripts",
|
|
33
|
+
"lib",
|
|
34
|
+
"configs",
|
|
35
|
+
"v2",
|
|
36
|
+
"web",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"CHANGELOG.md"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"lint": "bash -n ./clawspark",
|
|
43
|
+
"test": "bash ./tests/run.sh",
|
|
44
|
+
"pack:check": "npm pack --dry-run",
|
|
45
|
+
"release:patch": "bash ./scripts/release.sh patch",
|
|
46
|
+
"release:minor": "bash ./scripts/release.sh minor",
|
|
47
|
+
"release:major": "bash ./scripts/release.sh major"
|
|
48
|
+
},
|
|
49
|
+
"preferGlobal": true,
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"provenance": true
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18"
|
|
56
|
+
}
|
|
57
|
+
}
|