diff --git a/Dockerfile b/Dockerfile
index 73ea109..fe03d0b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -28,6 +28,23 @@ ENV NODE_PATH=/usr/local/lib/node_modules
ENV NODE_ENV=production
+# Install wacli (WhatsApp CLI) for the default OpenClaw wacli skill.
+ARG WACLI_VERSION="0.15.0"
+RUN ARCH=$(dpkg --print-architecture) && \
+ case "$ARCH" in \
+ amd64|arm64) WACLI_ARCH="$ARCH" ;; \
+ *) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
+ esac && \
+ TARBALL="wacli_${WACLI_VERSION}_linux_${WACLI_ARCH}.tar.gz" && \
+ BASE_URL="https://github.com/openclaw/wacli/releases/download/v${WACLI_VERSION}" && \
+ curl -fsSL "${BASE_URL}/${TARBALL}" -o "/tmp/${TARBALL}" && \
+ curl -fsSL "${BASE_URL}/checksums.txt" -o /tmp/wacli_checksums.txt && \
+ grep " ${TARBALL}$" /tmp/wacli_checksums.txt > /tmp/wacli_checksum.txt && \
+ (cd /tmp && sha256sum -c wacli_checksum.txt) && \
+ tar -xz -C /usr/local/bin -f "/tmp/${TARBALL}" wacli && \
+ chmod +x /usr/local/bin/wacli && \
+ rm "/tmp/${TARBALL}" /tmp/wacli_checksums.txt /tmp/wacli_checksum.txt
+
# Install ttyd (web terminal) - static binary from GitHub releases
# ttyd is not available in bookworm repos, so we download the pre-built binary
RUN ARCH=$(dpkg --print-architecture) && \
diff --git a/dappnode_package.json b/dappnode_package.json
index b2f41f4..e1da346 100644
--- a/dappnode_package.json
+++ b/dappnode_package.json
@@ -91,8 +91,8 @@
"type": "service",
"upstreamArg": "UPSTREAM_VERSION",
"upstreamRepo": "openclaw/openclaw",
- "upstreamVersion": "2026.6.11",
- "version": "0.1.3",
+ "upstreamVersion": "2026.7.1",
+ "version": "0.1.4",
"warnings": {
"onRemove": "Removing this package will delete all your OpenClaw configuration, conversation history, and cached data. Make sure to create a backup first."
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 9b099c8..1e91dce 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,7 +5,7 @@ services:
context: .
dockerfile: Dockerfile
args:
- UPSTREAM_VERSION: 2026.6.11
+ UPSTREAM_VERSION: 2026.7.1
image: openclaw.dnp.dappnode.eth:0.1.0
container_name: DAppNodePackage-openclaw.dnp.dappnode.eth
restart: unless-stopped
diff --git a/entrypoint.sh b/entrypoint.sh
index fb64f7d..e7263d4 100644
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -52,24 +52,74 @@ try {
if (!('dangerouslyDisableDeviceAuth' in cui)) { cui.dangerouslyDisableDeviceAuth = true; changed = true; }
if (auth.token !== envToken) { auth.token = envToken; changed = true; }
+ function isObject(value) {
+ return value && typeof value === 'object' && !Array.isArray(value);
+ }
+
+ function migrateTelegramStreaming(entry) {
+ if (!isObject(entry)) return false;
+ let entryChanged = false;
+ const legacyStreaming = entry.streaming;
+ const streaming = isObject(legacyStreaming) ? legacyStreaming : {};
+
+ if ('streaming' in entry && !isObject(legacyStreaming)) {
+ if (typeof legacyStreaming === 'boolean') {
+ streaming.mode = legacyStreaming ? 'partial' : 'off';
+ } else if (legacyStreaming != null) {
+ streaming.mode = String(legacyStreaming);
+ }
+ entry.streaming = streaming;
+ entryChanged = true;
+ }
+ if ('streamMode' in entry) {
+ if (!streaming.mode) streaming.mode = entry.streamMode;
+ delete entry.streamMode;
+ entryChanged = true;
+ }
+ if ('chunkMode' in entry) {
+ if (!('chunkMode' in streaming)) streaming.chunkMode = entry.chunkMode;
+ delete entry.chunkMode;
+ entryChanged = true;
+ }
+ if ('draftChunk' in entry) {
+ const preview = isObject(streaming.preview) ? streaming.preview : {};
+ if (!('chunk' in preview)) preview.chunk = entry.draftChunk;
+ streaming.preview = preview;
+ delete entry.draftChunk;
+ entryChanged = true;
+ }
+ if ('blockStreaming' in entry) {
+ const block = isObject(streaming.block) ? streaming.block : {};
+ if (!('enabled' in block)) block.enabled = entry.blockStreaming;
+ streaming.block = block;
+ delete entry.blockStreaming;
+ entryChanged = true;
+ }
+ if ('blockStreamingCoalesce' in entry) {
+ const block = isObject(streaming.block) ? streaming.block : {};
+ if (!('coalesce' in block)) block.coalesce = entry.blockStreamingCoalesce;
+ streaming.block = block;
+ delete entry.blockStreamingCoalesce;
+ entryChanged = true;
+ }
+ if (entryChanged) entry.streaming = streaming;
+ return entryChanged;
+ }
+
// Migrate legacy channel keys that openclaw doctor --fix cannot fully resolve in one pass:
- // streamMode (string) → streaming (string)
- // streaming (boolean) → streaming (string: true→\"partial\", false→\"off\")
+ // Telegram streamMode / scalar streaming / flat chunk keys → streaming.{mode,chunkMode,preview.chunk,block.*}
// discord.botToken → discord.token (Telegram keeps botToken; Discord uses token)
// discord guild channel: allow (bool) → enabled (bool)
const channels = config.channels || {};
for (const [chName, ch] of Object.entries(channels)) {
if (!ch || typeof ch !== 'object') continue;
- // streamMode → streaming string
- if ('streamMode' in ch) {
- ch.streaming = ch.streamMode;
- delete ch.streamMode;
- changed = true;
- }
- // boolean streaming → string
- if (typeof ch.streaming === 'boolean') {
- ch.streaming = ch.streaming ? 'partial' : 'off';
- changed = true;
+ if (chName === 'telegram') {
+ if (migrateTelegramStreaming(ch)) changed = true;
+ if (ch.accounts && typeof ch.accounts === 'object') {
+ for (const account of Object.values(ch.accounts)) {
+ if (migrateTelegramStreaming(account)) changed = true;
+ }
+ }
}
// Discord: botToken is not valid — migrate to token (plain string)
if (chName === 'discord' && 'botToken' in ch) {
@@ -104,6 +154,17 @@ fi
# ---------------------------------------------------------------------------
# Run doctor --fix for any remaining migrations not handled above
# ---------------------------------------------------------------------------
+echo "Pruning stale OpenClaw session entries..."
+MAIN_SESSION_STORE="$OPENCLAW_DIR/agents/main/sessions/sessions.json"
+mkdir -p "$(dirname "$MAIN_SESSION_STORE")"
+OPENCLAW_STATE_DIR="$OPENCLAW_DIR" openclaw sessions cleanup --store "$MAIN_SESSION_STORE" --enforce --fix-missing || true
+if [ -d "$OPENCLAW_DIR/agents" ]; then
+ find "$OPENCLAW_DIR/agents" -mindepth 3 -maxdepth 3 -path "*/sessions/sessions.json" -print | while IFS= read -r SESSION_STORE; do
+ [ "$SESSION_STORE" = "$MAIN_SESSION_STORE" ] && continue
+ OPENCLAW_STATE_DIR="$OPENCLAW_DIR" openclaw sessions cleanup --store "$SESSION_STORE" --enforce --fix-missing || true
+ done
+fi
+
echo "Running openclaw doctor --fix..."
OPENCLAW_STATE_DIR="$OPENCLAW_DIR" openclaw doctor --fix || true
diff --git a/setup-wizard/index.html b/setup-wizard/index.html
index 115dae2..3449c21 100644
--- a/setup-wizard/index.html
+++ b/setup-wizard/index.html
@@ -336,6 +336,48 @@
color: var(--error);
}
+ .nexus-auth-panel {
+ background: rgba(102, 126, 234, 0.08);
+ border: 1px solid rgba(102, 126, 234, 0.28);
+ border-radius: 10px;
+ padding: 1rem;
+ margin-bottom: 1.25rem;
+ }
+
+ .nexus-auth-panel .btn {
+ width: 100%;
+ }
+
+ .nexus-auth-note {
+ color: var(--text-muted);
+ font-size: 0.82rem;
+ line-height: 1.45;
+ margin-top: 0.75rem;
+ }
+
+ .nexus-auth-result {
+ display: none;
+ margin-top: 0.75rem;
+ padding: 10px 12px;
+ border-radius: 8px;
+ font-size: 0.85rem;
+ line-height: 1.45;
+ }
+
+ .nexus-auth-result.ok {
+ display: block;
+ background: rgba(52, 211, 153, 0.12);
+ border: 1px solid rgba(52, 211, 153, 0.3);
+ color: var(--success);
+ }
+
+ .nexus-auth-result.fail {
+ display: block;
+ background: rgba(248, 113, 113, 0.12);
+ border: 1px solid rgba(248, 113, 113, 0.3);
+ color: var(--error);
+ }
+
/* Review */
.config-preview {
background: var(--card);
@@ -1236,20 +1278,24 @@
Configuration Saved!
(async () => {
try {
const resp = await fetch("/api/config");
- if (!resp.ok) return;
- existingConfig = await resp.json();
- const providers = existingConfig.models && existingConfig.models.providers ? existingConfig.models.providers : {};
- existingProviderIds = Object.keys(providers);
+ if (resp.ok) {
+ existingConfig = await resp.json();
+ const providers = existingConfig.models && existingConfig.models.providers ? existingConfig.models.providers : {};
+ existingProviderIds = Object.keys(providers);
- if (existingProviderIds.length > 0) {
- goTo("home");
- } else {
- // Config exists but no providers yet — just prefill and continue
- document.getElementById("existing-config-banner").innerHTML =
- 'Existing configuration found. Your current settings have been loaded — edit any step and save to update.
';
- prefillFromConfig(existingConfig);
+ if (existingProviderIds.length > 0) {
+ goTo("home");
+ } else {
+ // Config exists but no providers yet — just prefill and continue
+ document.getElementById("existing-config-banner").innerHTML =
+ 'Existing configuration found. Your current settings have been loaded — edit any step and save to update.
';
+ prefillFromConfig(existingConfig);
+ }
}
} catch { }
+ finally {
+ await handleNexusAuthRedirect();
+ }
})();
function prefillFromConfig(cfg) {
@@ -1427,7 +1473,7 @@ Configuration Saved!
dmPolicy: "pairing",
groupPolicy: "allowlist",
groups: { "*": { requireMention: true } },
- streaming: "partial",
+ streaming: { mode: "partial" },
};
if (telegramUsers) {
config.channels.telegram.allowFrom = telegramUsers.split(",").map(s => s.trim()).filter(Boolean);
@@ -1596,13 +1642,18 @@ Configure DappNode Nexus
The gateway for AI builders who value privacy. Access top AI models through a single OpenAI-compatible API — filtered by privacy, speed, and reasoning level.
+
+
Login with Nexus & create API key
+
Creates a Nexus API key for this OpenClaw instance and fills it below.
Manage your keys .
+
+
Get 5€ in free credits to test private AI models — no commitment needed.
- Create your account at
nexus.dappnode.com , buy credits or subscribe, then paste your API key below.
+ Create your account at
nexus.dappnode.com , buy credits or subscribe, then use the login above or paste your API key below.
Nexus API Key
-
Found in your Nexus account under API keys.
+
Use Nexus login above, or paste a key from your Nexus account under API keys.
@@ -2094,22 +2145,22 @@ Configure ${p.name}
const resultEl = document.getElementById("nexus-fetch-result");
if (!apiKey) {
- resultEl.innerHTML = 'Enter your API key first.
';
+ if (resultEl) resultEl.innerHTML = 'Enter your API key first.
';
return;
}
- btn.disabled = true;
- icon.textContent = "\u23F3";
- resultEl.innerHTML = "";
+ if (btn) btn.disabled = true;
+ if (icon) icon.textContent = "\u23F3";
+ if (resultEl) resultEl.innerHTML = "";
try {
const resp = await fetch(`/api/nexus/models?apiKey=${encodeURIComponent(apiKey)}`);
const data = await resp.json();
if (!resp.ok) {
- resultEl.innerHTML = `Failed: ${data.error || "Unknown error"}
`;
- btn.disabled = false;
- icon.textContent = "\uD83D\uDD0D";
+ if (resultEl) resultEl.innerHTML = `Failed: ${data.error || "Unknown error"}
`;
+ if (btn) btn.disabled = false;
+ if (icon) icon.textContent = "\uD83D\uDD0D";
return;
}
@@ -2123,9 +2174,9 @@ Configure ${p.name}
}));
if (models.length === 0) {
- resultEl.innerHTML = 'No models found for this API key.
';
- btn.disabled = false;
- icon.textContent = "\uD83D\uDD0D";
+ if (resultEl) resultEl.innerHTML = 'No models found for this API key.
';
+ if (btn) btn.disabled = false;
+ if (icon) icon.textContent = "\uD83D\uDD0D";
return;
}
@@ -2134,20 +2185,101 @@ Configure ${p.name}
const select = document.getElementById("model-select");
if (select) {
+ const currentModel = select.value === "__custom"
+ ? (document.getElementById("custom-model") || {}).value
+ : select.value;
select.innerHTML = models.map(m => `${m.name} `).join("") +
'Custom model... ';
- select.value = models[0].id;
+ select.value = models.some(m => m.id === currentModel) ? currentModel : models[0].id;
const customField = document.getElementById("custom-model-field");
if (customField) customField.style.display = "none";
}
- resultEl.innerHTML = `${models.length} model${models.length !== 1 ? 's' : ''} available — select one below.
`;
+ if (resultEl) resultEl.innerHTML = `${models.length} model${models.length !== 1 ? 's' : ''} available — select one below.
`;
} catch (err) {
- resultEl.innerHTML = `Network error: ${err.message}
`;
+ if (resultEl) resultEl.innerHTML = `Network error: ${err.message}
`;
}
- btn.disabled = false;
- icon.textContent = "\uD83D\uDD0D";
+ if (btn) btn.disabled = false;
+ if (icon) icon.textContent = "\uD83D\uDD0D";
+ }
+
+ function showNexusAuthResult(kind, message) {
+ const el = document.getElementById("nexus-auth-result");
+ if (!el) return;
+ el.textContent = message;
+ el.className = "nexus-auth-result " + (kind === "ok" ? "ok" : "fail");
+ }
+
+ function removeNexusAuthParams() {
+ const next = new URL(window.location.href);
+ for (const key of ["nexus_auth", "nexus_message", "nexus_result", "nexus_mode"]) {
+ next.searchParams.delete(key);
+ }
+ window.history.replaceState(null, "", next.pathname + next.search + next.hash);
+ }
+
+ function startNexusLogin() {
+ const btn = document.getElementById("nexus-login-btn");
+ if (btn) {
+ btn.disabled = true;
+ btn.textContent = "Opening Nexus login...";
+ }
+ const current = new URL(window.location.href);
+ for (const key of ["nexus_auth", "nexus_message", "nexus_result", "nexus_mode"]) {
+ current.searchParams.delete(key);
+ }
+ current.searchParams.set("nexus_mode", wizardMode === "add-provider" ? "add-provider" : "update");
+ const returnTo = current.pathname + current.search + current.hash;
+ window.location.href = "/nexus/auth/start?returnTo=" + encodeURIComponent(returnTo || "/");
+ }
+
+ async function handleNexusAuthRedirect() {
+ const params = new URLSearchParams(window.location.search);
+ const status = params.get("nexus_auth");
+ if (!status) return;
+
+ wizardMode = params.get("nexus_mode") === "add-provider" ? "add-provider" : "update";
+ if (existingConfig && existingConfig.models && existingConfig.models.providers && existingConfig.models.providers.nexus) {
+ const providerCfg = existingConfig.models.providers.nexus;
+ const models = providerCfg.models || [];
+ existingConfig._loadedProvider = "nexus";
+ existingConfig._loadedApiKey = providerCfg.apiKey || "";
+ existingConfig._loadedModelId = models.length > 0 ? models[0].id : "";
+ }
+ selectProviderCard("nexus");
+ goTo(1);
+
+ try {
+ if (status === "error") {
+ showNexusAuthResult("fail", params.get("nexus_message") || "Nexus login failed. Please try again.");
+ return;
+ }
+
+ const resultId = params.get("nexus_result");
+ if (!resultId) {
+ showNexusAuthResult("fail", "Nexus login finished without an API key result. Please try again.");
+ return;
+ }
+
+ showNexusAuthResult("ok", "Creating Nexus API key...");
+ const resp = await fetch("/api/nexus/auth/result", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ id: resultId }),
+ });
+ const data = await resp.json().catch(() => ({}));
+ if (!resp.ok) throw new Error(data.error || "Could not retrieve the generated Nexus API key");
+
+ const input = document.getElementById("api-key");
+ if (input) input.value = data.apiKey || "";
+ await fetchNexusModels();
+ showNexusAuthResult("ok", "Nexus API key created. Choose a model and continue.");
+ } catch (err) {
+ showNexusAuthResult("fail", err.message || "Nexus login failed. Please try again.");
+ } finally {
+ removeNexusAuthParams();
+ }
}
async function probeOllama() {
@@ -2694,4 +2826,4 @@ Configure ${p.name}