mlclaw 0.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/.agents/skills/mlclaw/SKILL.md +214 -0
- package/.agents/skills/mlclaw/agents/openai.yaml +4 -0
- package/.gitattributes +35 -0
- package/Dockerfile +45 -0
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/assets/mlclaw.svg +143 -0
- package/dist/hf-state-sync.js +9532 -0
- package/dist/mlclaw-space-runtime.js +5010 -0
- package/dist/mlclaw.mjs +16502 -0
- package/entrypoint.sh +87 -0
- package/mlclaw.ps1 +108 -0
- package/mlclaw.sh +117 -0
- package/openclaw.default.json +67 -0
- package/package.json +66 -0
- package/scripts/configure-huggingface-model.mjs +86 -0
- package/scripts/configure-telegram.mjs +55 -0
- package/scripts/report-telegram-probe.mjs +18 -0
- package/space/README.md +42 -0
- package/src/hf-bucket-client/client.ts +217 -0
- package/src/hf-state-sync/archive.ts +137 -0
- package/src/hf-state-sync/cli.ts +92 -0
- package/src/hf-state-sync/hub.ts +81 -0
- package/src/hf-state-sync/manifest.ts +67 -0
- package/src/hf-state-sync/paths.ts +73 -0
- package/src/hf-state-sync/restore.ts +109 -0
- package/src/hf-state-sync/snapshot.ts +133 -0
- package/src/hf-state-sync/sqlite.ts +57 -0
- package/src/hf-state-sync/supervise.ts +256 -0
- package/src/vendor/hfjs-xet/error.ts +52 -0
- package/src/vendor/hfjs-xet/types/public.ts +207 -0
- package/src/vendor/hfjs-xet/utils/ChunkCache.ts +102 -0
- package/src/vendor/hfjs-xet/utils/RangeList.ts +182 -0
- package/src/vendor/hfjs-xet/utils/SplicedBlob.ts +249 -0
- package/src/vendor/hfjs-xet/utils/XetBlob.ts +732 -0
- package/src/vendor/hfjs-xet/utils/checkCredentials.ts +21 -0
- package/src/vendor/hfjs-xet/utils/combineUint8Arrays.ts +13 -0
- package/src/vendor/hfjs-xet/utils/createXorbs.ts +782 -0
- package/src/vendor/hfjs-xet/utils/shardParser.ts +152 -0
- package/src/vendor/hfjs-xet/utils/sum.ts +9 -0
- package/src/vendor/hfjs-xet/utils/uploadShards.ts +443 -0
- package/src/vendor/hfjs-xet/utils/xetWriteToken.ts +101 -0
- package/src/vendor/hfjs-xet/vendor/lz4js/index.ts +540 -0
- package/src/vendor/hfjs-xet/vendor/lz4js/util.ts +57 -0
- package/src/vendor/hfjs-xet/vendor/lz4js/xxh32.ts +99 -0
- package/src/vendor/hfjs-xet/vendor/type-fest/basic.ts +34 -0
- package/tsconfig.json +16 -0
package/entrypoint.sh
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
LIVE_DIR="${OPENCLAW_LIVE_DIR:-/tmp/openclaw-live}"
|
|
5
|
+
|
|
6
|
+
if [ "${MLCLAW_GATEWAY_DISABLED:-0}" = "1" ]; then
|
|
7
|
+
echo "[mlclaw] gateway disabled"
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
if [ -z "${HUGGINGFACE_HUB_TOKEN:-}" ] && [ -n "${HF_TOKEN:-}" ]; then
|
|
12
|
+
export HUGGINGFACE_HUB_TOKEN="$HF_TOKEN"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# State, workspace, and config paths are ALWAYS derived from the live dir,
|
|
16
|
+
# never inherited: older deployments set OPENCLAW_STATE_DIR=/data/... as Space
|
|
17
|
+
# variables, and any state written outside the live dir would be invisible to
|
|
18
|
+
# snapshot/restore — the bucket would back up an empty tree.
|
|
19
|
+
export OPENCLAW_STATE_DIR="$LIVE_DIR/.openclaw"
|
|
20
|
+
export OPENCLAW_WORKSPACE_DIR="$LIVE_DIR/workspace"
|
|
21
|
+
export OPENCLAW_CONFIG_PATH="$LIVE_DIR/.openclaw/openclaw.json"
|
|
22
|
+
export OPENCLAW_GATEWAY_PORT="${MLCLAW_OPENCLAW_PORT:-7861}"
|
|
23
|
+
STATE_DIR="$OPENCLAW_STATE_DIR"
|
|
24
|
+
WORKSPACE_DIR="$OPENCLAW_WORKSPACE_DIR"
|
|
25
|
+
CONFIG_PATH="$OPENCLAW_CONFIG_PATH"
|
|
26
|
+
|
|
27
|
+
# Restore durable state from the bucket BEFORE creating any live dirs: the
|
|
28
|
+
# restore target is the live dir itself and must not exist yet. Fails the boot
|
|
29
|
+
# if the bucket has state but its manifest or every snapshot is corrupt (never
|
|
30
|
+
# silently start fresh then snapshot over a bucket that still holds data).
|
|
31
|
+
echo "[hf-state-sync] starting restore"
|
|
32
|
+
RESTORE_TIMEOUT_SECONDS="${MLCLAW_RESTORE_TIMEOUT_SECONDS:-180}"
|
|
33
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
34
|
+
timeout "${RESTORE_TIMEOUT_SECONDS}s" node /app/hf-state-sync.js restore
|
|
35
|
+
else
|
|
36
|
+
node /app/hf-state-sync.js restore
|
|
37
|
+
fi
|
|
38
|
+
echo "[hf-state-sync] restore complete"
|
|
39
|
+
|
|
40
|
+
mkdir -p "$LIVE_DIR" "$WORKSPACE_DIR" "$STATE_DIR"
|
|
41
|
+
chown -R node:node "$LIVE_DIR"
|
|
42
|
+
|
|
43
|
+
if [ -n "${OPENCLAW_AGENT_NAME:-}" ]; then
|
|
44
|
+
printf "%s\n" "$OPENCLAW_AGENT_NAME" > "$STATE_DIR/agent-name.txt"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if [ ! -f "$CONFIG_PATH" ]; then
|
|
48
|
+
cp /app/openclaw.default.json "$CONFIG_PATH"
|
|
49
|
+
fi
|
|
50
|
+
chown -R node:node "$LIVE_DIR"
|
|
51
|
+
|
|
52
|
+
if [ -n "${OPENCLAW_MODEL:-}" ]; then
|
|
53
|
+
echo "[huggingface-config] configuring selected Hugging Face model"
|
|
54
|
+
gosu node node /app/scripts/configure-huggingface-model.mjs "$CONFIG_PATH"
|
|
55
|
+
echo "[huggingface-config] Hugging Face model configured"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${TELEGRAM_ALLOWED_USERS:-}" ]; then
|
|
59
|
+
echo "[telegram-config] configuring Telegram channel"
|
|
60
|
+
gosu node node /app/scripts/configure-telegram.mjs "$CONFIG_PATH" "$TELEGRAM_ALLOWED_USERS"
|
|
61
|
+
echo "[telegram-config] Telegram channel configured"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ "${OPENCLAW_TELEGRAM_CONNECTIVITY_PROBE:-0}" = "1" ]; then
|
|
65
|
+
if command -v curl >/dev/null 2>&1; then
|
|
66
|
+
PROBE_OUT="/tmp/openclaw-telegram-probe.json"
|
|
67
|
+
PROBE_PROXY=()
|
|
68
|
+
PROBE_API_ROOT="${TELEGRAM_API_ROOT:-https://api.telegram.org}"
|
|
69
|
+
PROBE_API_ROOT="${PROBE_API_ROOT%/}"
|
|
70
|
+
if [ -n "${TELEGRAM_PROXY:-}" ]; then
|
|
71
|
+
PROBE_PROXY=(--proxy "$TELEGRAM_PROXY")
|
|
72
|
+
fi
|
|
73
|
+
if curl -fsS --connect-timeout 20 --max-time 30 "${PROBE_PROXY[@]}" \
|
|
74
|
+
"${PROBE_API_ROOT}/bot${TELEGRAM_BOT_TOKEN}/getMe" \
|
|
75
|
+
-o "$PROBE_OUT"; then
|
|
76
|
+
gosu node node /app/scripts/report-telegram-probe.mjs "$PROBE_OUT" || true
|
|
77
|
+
else
|
|
78
|
+
echo "[telegram-probe] curl getMe failed"
|
|
79
|
+
fi
|
|
80
|
+
rm -f "$PROBE_OUT"
|
|
81
|
+
else
|
|
82
|
+
echo "[telegram-probe] curl is unavailable; skipping"
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
chown -R node:node "$LIVE_DIR"
|
|
87
|
+
exec gosu node node /app/hf-state-sync.js supervise -- node /app/mlclaw-space-runtime.js
|
package/mlclaw.ps1
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
$ErrorActionPreference = "Stop"
|
|
2
|
+
|
|
3
|
+
$MinNodeMajor = if ($env:MLCLAW_MIN_NODE_MAJOR) { [int]$env:MLCLAW_MIN_NODE_MAJOR } else { 22 }
|
|
4
|
+
$NodeVersion = if ($env:MLCLAW_NODE_VERSION) { $env:MLCLAW_NODE_VERSION } else { "v24.16.0" }
|
|
5
|
+
$PackageSpec = if ($env:MLCLAW_NPM_SPEC) { $env:MLCLAW_NPM_SPEC } else { "mlclaw" }
|
|
6
|
+
$CacheRoot = if ($env:MLCLAW_CACHE_DIR) {
|
|
7
|
+
$env:MLCLAW_CACHE_DIR
|
|
8
|
+
} elseif ($env:LOCALAPPDATA) {
|
|
9
|
+
Join-Path $env:LOCALAPPDATA "mlclaw"
|
|
10
|
+
} else {
|
|
11
|
+
Join-Path $HOME ".cache\mlclaw"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function Get-NodeMajor($NodeBin) {
|
|
15
|
+
try {
|
|
16
|
+
$major = & $NodeBin -p 'Number(process.versions.node.split(".")[0])'
|
|
17
|
+
return [int]$major
|
|
18
|
+
} catch {
|
|
19
|
+
return 0
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Test-CompatibleNode($NodeBin) {
|
|
24
|
+
return (Get-NodeMajor $NodeBin) -ge $MinNodeMajor
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Get-SystemNode {
|
|
28
|
+
$cmd = Get-Command node -ErrorAction SilentlyContinue
|
|
29
|
+
if ($cmd -and (Test-CompatibleNode $cmd.Source)) {
|
|
30
|
+
return $cmd.Source
|
|
31
|
+
}
|
|
32
|
+
return $null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Get-NodeArch {
|
|
36
|
+
if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq "Arm64") {
|
|
37
|
+
return "arm64"
|
|
38
|
+
}
|
|
39
|
+
return "x64"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function Install-CachedNode {
|
|
43
|
+
$arch = Get-NodeArch
|
|
44
|
+
$archiveName = "node-$NodeVersion-win-$arch"
|
|
45
|
+
$targetDir = Join-Path $CacheRoot "node\$NodeVersion-win-$arch"
|
|
46
|
+
$nodeBin = Join-Path $targetDir "node.exe"
|
|
47
|
+
if ((Test-Path $nodeBin) -and (Test-CompatibleNode $nodeBin)) {
|
|
48
|
+
return $targetDir
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
$parent = Split-Path $targetDir -Parent
|
|
52
|
+
New-Item -ItemType Directory -Force -Path $parent | Out-Null
|
|
53
|
+
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("mlclaw-node-" + [System.Guid]::NewGuid().ToString("N"))
|
|
54
|
+
New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
|
|
55
|
+
try {
|
|
56
|
+
$zipPath = Join-Path $tempDir "node.zip"
|
|
57
|
+
$url = "https://nodejs.org/dist/$NodeVersion/$archiveName.zip"
|
|
58
|
+
Write-Error "mlclaw: installing Node.js $NodeVersion into $targetDir" -ErrorAction Continue
|
|
59
|
+
Invoke-WebRequest -Uri $url -OutFile $zipPath
|
|
60
|
+
Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force
|
|
61
|
+
if (Test-Path $targetDir) {
|
|
62
|
+
Remove-Item -Recurse -Force $targetDir
|
|
63
|
+
}
|
|
64
|
+
Move-Item (Join-Path $tempDir $archiveName) $targetDir
|
|
65
|
+
} finally {
|
|
66
|
+
Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
|
|
67
|
+
}
|
|
68
|
+
return $targetDir
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Invoke-MLClaw($NodeBin, [string[]]$CliArgs) {
|
|
72
|
+
$nodeDir = Split-Path $NodeBin -Parent
|
|
73
|
+
$env:PATH = "$nodeDir;$env:PATH"
|
|
74
|
+
$npmCli = Join-Path $nodeDir "node_modules\npm\bin\npm-cli.js"
|
|
75
|
+
if (-not (Test-Path $npmCli)) {
|
|
76
|
+
$npmCommand = Get-Command npm -ErrorAction SilentlyContinue
|
|
77
|
+
if (-not $npmCommand) {
|
|
78
|
+
throw "npm was not found for Node runtime $NodeBin"
|
|
79
|
+
}
|
|
80
|
+
& $npmCommand.Source exec --yes --package $PackageSpec -- mlclaw @CliArgs
|
|
81
|
+
exit $LASTEXITCODE
|
|
82
|
+
}
|
|
83
|
+
& $NodeBin $npmCli exec --yes --package $PackageSpec -- mlclaw @CliArgs
|
|
84
|
+
exit $LASTEXITCODE
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Test-NodeHasNpm($NodeBin) {
|
|
88
|
+
$nodeDir = Split-Path $NodeBin -Parent
|
|
89
|
+
$npmCli = Join-Path $nodeDir "node_modules\npm\bin\npm-cli.js"
|
|
90
|
+
if (Test-Path $npmCli) {
|
|
91
|
+
return $true
|
|
92
|
+
}
|
|
93
|
+
$oldPath = $env:PATH
|
|
94
|
+
try {
|
|
95
|
+
$env:PATH = "$nodeDir;$env:PATH"
|
|
96
|
+
return [bool](Get-Command npm -ErrorAction SilentlyContinue)
|
|
97
|
+
} finally {
|
|
98
|
+
$env:PATH = $oldPath
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
$node = Get-SystemNode
|
|
103
|
+
if ($node -and (Test-NodeHasNpm $node)) {
|
|
104
|
+
Invoke-MLClaw $node $args
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
$nodeDir = Install-CachedNode
|
|
108
|
+
Invoke-MLClaw (Join-Path $nodeDir "node.exe") $args
|
package/mlclaw.sh
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
MIN_NODE_MAJOR="${MLCLAW_MIN_NODE_MAJOR:-22}"
|
|
5
|
+
NODE_VERSION="${MLCLAW_NODE_VERSION:-v24.16.0}"
|
|
6
|
+
PACKAGE_SPEC="${MLCLAW_NPM_SPEC:-mlclaw}"
|
|
7
|
+
CACHE_ROOT="${MLCLAW_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/mlclaw}"
|
|
8
|
+
|
|
9
|
+
die() {
|
|
10
|
+
printf 'mlclaw: %s\n' "$*" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
node_major() {
|
|
15
|
+
"$1" -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || printf '0\n'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
node_is_compatible() {
|
|
19
|
+
local node_bin="$1"
|
|
20
|
+
local major
|
|
21
|
+
major="$(node_major "$node_bin")"
|
|
22
|
+
[ "$major" -ge "$MIN_NODE_MAJOR" ]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
system_node() {
|
|
26
|
+
if command -v node >/dev/null 2>&1 && node_is_compatible "$(command -v node)"; then
|
|
27
|
+
command -v node
|
|
28
|
+
fi
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
platform_name() {
|
|
32
|
+
case "$(uname -s)" in
|
|
33
|
+
Darwin) printf 'darwin' ;;
|
|
34
|
+
Linux) printf 'linux' ;;
|
|
35
|
+
*) die "unsupported operating system: $(uname -s)" ;;
|
|
36
|
+
esac
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
arch_name() {
|
|
40
|
+
case "$(uname -m)" in
|
|
41
|
+
x86_64 | amd64) printf 'x64' ;;
|
|
42
|
+
arm64 | aarch64) printf 'arm64' ;;
|
|
43
|
+
*) die "unsupported CPU architecture: $(uname -m)" ;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
cached_node_dir() {
|
|
48
|
+
printf '%s/node/%s-%s-%s\n' "$CACHE_ROOT" "$NODE_VERSION" "$(platform_name)" "$(arch_name)"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
install_node() {
|
|
52
|
+
command -v curl >/dev/null 2>&1 || die "curl is required to install Node.js"
|
|
53
|
+
command -v tar >/dev/null 2>&1 || die "tar is required to install Node.js"
|
|
54
|
+
|
|
55
|
+
local os arch archive_name url temp_dir target_dir
|
|
56
|
+
os="$(platform_name)"
|
|
57
|
+
arch="$(arch_name)"
|
|
58
|
+
archive_name="node-${NODE_VERSION}-${os}-${arch}"
|
|
59
|
+
url="https://nodejs.org/dist/${NODE_VERSION}/${archive_name}.tar.xz"
|
|
60
|
+
target_dir="$(cached_node_dir)"
|
|
61
|
+
|
|
62
|
+
if [ -x "$target_dir/bin/node" ] && node_is_compatible "$target_dir/bin/node"; then
|
|
63
|
+
printf '%s\n' "$target_dir"
|
|
64
|
+
return
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/mlclaw-node.XXXXXX")"
|
|
68
|
+
mkdir -p "$(dirname "$target_dir")"
|
|
69
|
+
printf 'mlclaw: installing Node.js %s into %s\n' "$NODE_VERSION" "$target_dir" >&2
|
|
70
|
+
if ! curl -fsSL "$url" -o "$temp_dir/node.tar.xz"; then
|
|
71
|
+
rm -rf "$temp_dir"
|
|
72
|
+
die "failed to download Node.js from $url"
|
|
73
|
+
fi
|
|
74
|
+
if ! tar -xJf "$temp_dir/node.tar.xz" -C "$temp_dir"; then
|
|
75
|
+
rm -rf "$temp_dir"
|
|
76
|
+
die "failed to unpack Node.js archive"
|
|
77
|
+
fi
|
|
78
|
+
rm -rf "$target_dir"
|
|
79
|
+
mv "$temp_dir/$archive_name" "$target_dir"
|
|
80
|
+
rm -rf "$temp_dir"
|
|
81
|
+
printf '%s\n' "$target_dir"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
run_with_node() {
|
|
85
|
+
local node_bin="$1"
|
|
86
|
+
shift
|
|
87
|
+
local npm_cli node_path
|
|
88
|
+
node_path="$(dirname "$node_bin")"
|
|
89
|
+
npm_cli="$(dirname "$node_bin")/../lib/node_modules/npm/bin/npm-cli.js"
|
|
90
|
+
if [ -f "$npm_cli" ]; then
|
|
91
|
+
PATH="$node_path:$PATH" exec "$node_bin" "$npm_cli" exec --yes --package "$PACKAGE_SPEC" -- mlclaw "$@"
|
|
92
|
+
fi
|
|
93
|
+
if command -v npm >/dev/null 2>&1; then
|
|
94
|
+
PATH="$node_path:$PATH" exec npm exec --yes --package "$PACKAGE_SPEC" -- mlclaw "$@"
|
|
95
|
+
fi
|
|
96
|
+
die "npm was not found for Node runtime $node_bin"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
node_has_npm() {
|
|
100
|
+
local node_bin="$1"
|
|
101
|
+
local node_path npm_cli
|
|
102
|
+
node_path="$(dirname "$node_bin")"
|
|
103
|
+
npm_cli="$(dirname "$node_bin")/../lib/node_modules/npm/bin/npm-cli.js"
|
|
104
|
+
[ -f "$npm_cli" ] || PATH="$node_path:$PATH" command -v npm >/dev/null 2>&1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main() {
|
|
108
|
+
local node_bin node_dir
|
|
109
|
+
node_bin="$(system_node || true)"
|
|
110
|
+
if [ -n "$node_bin" ] && node_has_npm "$node_bin"; then
|
|
111
|
+
run_with_node "$node_bin" "$@"
|
|
112
|
+
fi
|
|
113
|
+
node_dir="$(install_node)"
|
|
114
|
+
run_with_node "$node_dir/bin/node" "$@"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
main "$@"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agents": {
|
|
3
|
+
"defaults": {
|
|
4
|
+
"model": {
|
|
5
|
+
"primary": "${OPENCLAW_MODEL}"
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"gateway": {
|
|
10
|
+
"mode": "local",
|
|
11
|
+
"bind": "loopback",
|
|
12
|
+
"port": 7861,
|
|
13
|
+
"auth": {
|
|
14
|
+
"mode": "trusted-proxy",
|
|
15
|
+
"trustedProxy": {
|
|
16
|
+
"userHeader": "x-forwarded-user",
|
|
17
|
+
"requiredHeaders": ["x-forwarded-proto", "x-forwarded-host"],
|
|
18
|
+
"allowLoopback": true
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"trustedProxies": ["127.0.0.1", "::1"],
|
|
22
|
+
"controlUi": {
|
|
23
|
+
"dangerouslyDisableDeviceAuth": true,
|
|
24
|
+
"allowedOrigins": ["${MLCLAW_PUBLIC_URL}"]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"channels": {
|
|
28
|
+
"telegram": {
|
|
29
|
+
"enabled": false,
|
|
30
|
+
"botToken": "${TELEGRAM_BOT_TOKEN}",
|
|
31
|
+
"dmPolicy": "allowlist",
|
|
32
|
+
"allowFrom": [],
|
|
33
|
+
"timeoutSeconds": 45,
|
|
34
|
+
"pollingStallThresholdMs": 60000,
|
|
35
|
+
"commands": {
|
|
36
|
+
"native": false
|
|
37
|
+
},
|
|
38
|
+
"network": {
|
|
39
|
+
"autoSelectFamily": false,
|
|
40
|
+
"dnsResultOrder": "ipv4first"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"messages": {
|
|
45
|
+
"queue": {
|
|
46
|
+
"byChannel": {
|
|
47
|
+
"telegram": "collect"
|
|
48
|
+
},
|
|
49
|
+
"debounceMsByChannel": {
|
|
50
|
+
"telegram": 1500
|
|
51
|
+
},
|
|
52
|
+
"cap": 20,
|
|
53
|
+
"drop": "summarize"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"session": {
|
|
57
|
+
"dmScope": "per-channel-peer",
|
|
58
|
+
"reset": {
|
|
59
|
+
"mode": "daily",
|
|
60
|
+
"atHour": 4,
|
|
61
|
+
"idleMinutes": 1440
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"cron": {
|
|
65
|
+
"enabled": true
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mlclaw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/osolmaz/mlclaw"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/osolmaz/mlclaw#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/osolmaz/mlclaw/issues"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"mlclaw": "dist/mlclaw.mjs"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
".agents/skills/",
|
|
19
|
+
".gitattributes",
|
|
20
|
+
"assets/",
|
|
21
|
+
"dist/hf-state-sync.js",
|
|
22
|
+
"dist/mlclaw.mjs",
|
|
23
|
+
"dist/mlclaw-space-runtime.js",
|
|
24
|
+
"Dockerfile",
|
|
25
|
+
"entrypoint.sh",
|
|
26
|
+
"mlclaw.ps1",
|
|
27
|
+
"mlclaw.sh",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"openclaw.default.json",
|
|
30
|
+
"README.md",
|
|
31
|
+
"scripts/configure-telegram.mjs",
|
|
32
|
+
"scripts/configure-huggingface-model.mjs",
|
|
33
|
+
"scripts/report-telegram-probe.mjs",
|
|
34
|
+
"space/",
|
|
35
|
+
"src/hf-bucket-client/",
|
|
36
|
+
"src/hf-state-sync/",
|
|
37
|
+
"src/vendor/",
|
|
38
|
+
"tsconfig.json"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "npm run build:state-sync && npm run build:space-runtime && npm run build:mlclaw",
|
|
42
|
+
"build:state-sync": "esbuild src/hf-state-sync/cli.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/hf-state-sync.js --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\"",
|
|
43
|
+
"build:space-runtime": "esbuild src/mlclaw-space-runtime/cli.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/mlclaw-space-runtime.js --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\"",
|
|
44
|
+
"build:mlclaw": "esbuild src/mlclaw/cli.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/mlclaw.mjs --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\"",
|
|
45
|
+
"build:probe": "esbuild scripts/parity-probe.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/parity-probe.mjs --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\"",
|
|
46
|
+
"pack:check": "node scripts/check-package.mjs",
|
|
47
|
+
"prepack": "npm run build",
|
|
48
|
+
"check:secrets": "node scripts/check-secrets.mjs",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@clack/prompts": "^1.4.0",
|
|
54
|
+
"@huggingface/splitmix64-wasm": "^0.0.1",
|
|
55
|
+
"@huggingface/xetchunk-wasm": "^0.0.6",
|
|
56
|
+
"commander": "^14.0.3",
|
|
57
|
+
"skillflag": "^0.2.0",
|
|
58
|
+
"zod": "^3.24.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^24.0.0",
|
|
62
|
+
"esbuild": "^0.25.0",
|
|
63
|
+
"typescript": "^5.7.0",
|
|
64
|
+
"vitest": "^3.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
const [configPath] = process.argv.slice(2);
|
|
4
|
+
|
|
5
|
+
if (!configPath) {
|
|
6
|
+
console.error("Usage: configure-huggingface-model.mjs <config-path>");
|
|
7
|
+
process.exit(2);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const modelRef = (process.env.OPENCLAW_MODEL || "").trim();
|
|
11
|
+
const prefix = "huggingface/";
|
|
12
|
+
|
|
13
|
+
if (!modelRef.startsWith(prefix)) {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const providerModelId = modelRef.slice(prefix.length).trim();
|
|
18
|
+
if (!providerModelId) {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function displayNameFromModelId(id) {
|
|
23
|
+
const withoutPolicy = id.replace(/:(cheapest|fastest)$/i, "");
|
|
24
|
+
const base = withoutPolicy.split("/").pop() || withoutPolicy;
|
|
25
|
+
return base.replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isLikelyImageModel(id) {
|
|
29
|
+
const lower = id.toLowerCase();
|
|
30
|
+
return (
|
|
31
|
+
lower.includes("-vl") ||
|
|
32
|
+
lower.includes("vision") ||
|
|
33
|
+
lower.includes("multimodal") ||
|
|
34
|
+
lower.includes("gemma-3") ||
|
|
35
|
+
lower.includes("gemma-4") ||
|
|
36
|
+
lower.includes("llama-4")
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function contextWindowForModel(id) {
|
|
41
|
+
const lower = id.toLowerCase();
|
|
42
|
+
if (lower.includes("gemma-4") || lower.includes("qwen3.6")) return 262144;
|
|
43
|
+
if (lower.includes("qwen3-8b") || lower.includes("qwen3-14b")) return 40960;
|
|
44
|
+
return 131072;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
48
|
+
config.models ||= {};
|
|
49
|
+
config.models.providers ||= {};
|
|
50
|
+
config.models.providers.huggingface ||= {};
|
|
51
|
+
|
|
52
|
+
const provider = config.models.providers.huggingface;
|
|
53
|
+
provider.baseUrl ||= "https://router.huggingface.co/v1";
|
|
54
|
+
provider.api ||= "openai-completions";
|
|
55
|
+
provider.models = Array.isArray(provider.models) ? provider.models : [];
|
|
56
|
+
|
|
57
|
+
const defaultEntry = {
|
|
58
|
+
id: providerModelId,
|
|
59
|
+
name: displayNameFromModelId(providerModelId),
|
|
60
|
+
input: isLikelyImageModel(providerModelId) ? ["text", "image"] : ["text"],
|
|
61
|
+
contextWindow: contextWindowForModel(providerModelId),
|
|
62
|
+
maxTokens: 8192,
|
|
63
|
+
reasoning: /r1|reason|thinking|reasoner|qwq/i.test(providerModelId),
|
|
64
|
+
cost: {
|
|
65
|
+
input: 0,
|
|
66
|
+
output: 0,
|
|
67
|
+
cacheRead: 0,
|
|
68
|
+
cacheWrite: 0,
|
|
69
|
+
},
|
|
70
|
+
api: "openai-completions",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const existingIndex = provider.models.findIndex(
|
|
74
|
+
(entry) => entry && typeof entry === "object" && entry.id === providerModelId,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (existingIndex >= 0) {
|
|
78
|
+
provider.models[existingIndex] = {
|
|
79
|
+
...defaultEntry,
|
|
80
|
+
...provider.models[existingIndex],
|
|
81
|
+
};
|
|
82
|
+
} else {
|
|
83
|
+
provider.models.push(defaultEntry);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
const [configPath, allowedUsersRaw] = process.argv.slice(2);
|
|
4
|
+
|
|
5
|
+
if (!configPath) {
|
|
6
|
+
console.error("Usage: configure-telegram.mjs <config-path> <allowed-users>");
|
|
7
|
+
process.exit(2);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const allowedUsers = (allowedUsersRaw || "")
|
|
11
|
+
.split(",")
|
|
12
|
+
.map((value) => value.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
const telegramProxy = (process.env.TELEGRAM_PROXY || "").trim();
|
|
15
|
+
const telegramApiRoot = (process.env.TELEGRAM_API_ROOT || "").trim().replace(/\/+$/, "");
|
|
16
|
+
|
|
17
|
+
if (allowedUsers.length === 0) {
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
22
|
+
config.channels ||= {};
|
|
23
|
+
config.channels.telegram ||= {};
|
|
24
|
+
config.messages ||= {};
|
|
25
|
+
config.messages.queue ||= {};
|
|
26
|
+
config.messages.queue.byChannel ||= {};
|
|
27
|
+
config.messages.queue.debounceMsByChannel ||= {};
|
|
28
|
+
|
|
29
|
+
config.messages.queue.byChannel.telegram ||= "collect";
|
|
30
|
+
config.messages.queue.debounceMsByChannel.telegram ??= 1500;
|
|
31
|
+
config.messages.queue.cap ??= 20;
|
|
32
|
+
config.messages.queue.drop ||= "summarize";
|
|
33
|
+
|
|
34
|
+
Object.assign(config.channels.telegram, {
|
|
35
|
+
enabled: true,
|
|
36
|
+
botToken: "${TELEGRAM_BOT_TOKEN}",
|
|
37
|
+
dmPolicy: "allowlist",
|
|
38
|
+
allowFrom: allowedUsers,
|
|
39
|
+
timeoutSeconds: 45,
|
|
40
|
+
pollingStallThresholdMs: 60000,
|
|
41
|
+
commands: {
|
|
42
|
+
...(config.channels.telegram.commands || {}),
|
|
43
|
+
native: false
|
|
44
|
+
},
|
|
45
|
+
network: {
|
|
46
|
+
...(config.channels.telegram.network || {}),
|
|
47
|
+
autoSelectFamily: false,
|
|
48
|
+
dnsResultOrder: "ipv4first"
|
|
49
|
+
},
|
|
50
|
+
...(telegramProxy ? { proxy: "${TELEGRAM_PROXY}" } : {}),
|
|
51
|
+
...(telegramApiRoot ? { apiRoot: "${TELEGRAM_API_ROOT}" } : {}),
|
|
52
|
+
groups: config.channels.telegram.groups || {}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
const [probePath] = process.argv.slice(2);
|
|
4
|
+
|
|
5
|
+
if (!probePath) {
|
|
6
|
+
process.exit(0);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const payload = JSON.parse(fs.readFileSync(probePath, "utf8"));
|
|
11
|
+
if (payload?.ok && payload?.result?.username) {
|
|
12
|
+
console.log(`[telegram-probe] curl getMe ok (@${payload.result.username})`);
|
|
13
|
+
} else {
|
|
14
|
+
console.log("[telegram-probe] curl getMe returned an unexpected response");
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
console.log("[telegram-probe] curl getMe response could not be parsed");
|
|
18
|
+
}
|
package/space/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ML Claw
|
|
3
|
+
emoji: 🦞
|
|
4
|
+
colorFrom: yellow
|
|
5
|
+
colorTo: red
|
|
6
|
+
sdk: docker
|
|
7
|
+
app_port: 7860
|
|
8
|
+
hf_oauth: true
|
|
9
|
+
hf_oauth_expiration_minutes: 43200
|
|
10
|
+
pinned: false
|
|
11
|
+
secrets:
|
|
12
|
+
- HF_TOKEN
|
|
13
|
+
- MLCLAW_SESSION_SECRET
|
|
14
|
+
- OPENAI_API_KEY
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# ML Claw
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img src="assets/mlclaw.svg" alt="ML Claw" width="160">
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
ML Claw runs OpenClaw in a Hugging Face Space with a browser gateway protected
|
|
24
|
+
by Hugging Face OAuth. Duplicate this Space or use `mlclaw bootstrap` to create
|
|
25
|
+
your own deployment.
|
|
26
|
+
|
|
27
|
+
The public Space process is an ML Claw proxy. It authenticates the signed-in
|
|
28
|
+
Hugging Face user, then forwards browser traffic to OpenClaw on loopback using
|
|
29
|
+
OpenClaw trusted-proxy auth. The browser never receives an OpenClaw gateway
|
|
30
|
+
token.
|
|
31
|
+
|
|
32
|
+
Durable state lives in a private Hugging Face Storage Bucket configured by
|
|
33
|
+
`OPENCLAW_HF_STATE_BUCKET`. The bucket is not mounted as a live filesystem;
|
|
34
|
+
`hf-state-sync` restores verified snapshots on boot and uploads new snapshots
|
|
35
|
+
during runtime and shutdown.
|
|
36
|
+
|
|
37
|
+
Manage an existing deployment from your machine with:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
mlclaw doctor <owner/space> --fix
|
|
41
|
+
mlclaw update <owner/space>
|
|
42
|
+
```
|