limbo-ai 1.31.0 → 2026.4.1
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/Dockerfile +116 -0
- package/RELEASES.md +32 -0
- package/cli.js +157 -18
- package/lib/telegram-notify.js +97 -0
- package/lib/wakeup.js +206 -0
- package/mcp-server/index.js +174 -1
- package/mcp-server/tools/google-calendar.js +277 -0
- package/mcp-server/tools/update-instance.js +59 -0
- package/migrations/index.js +199 -0
- package/migrations/package.json +3 -0
- package/migrations/versions/001-initial-schema.js +47 -0
- package/migrations/versions/002-unified-note-types.js +150 -0
- package/migrations/versions/003-zeroclaw-migration.js +24 -0
- package/migrations/versions/004-assets-directory.js +25 -0
- package/migrations/versions/005-fts5-search.js +137 -0
- package/migrations/versions/006-fts5-dedupe.js +155 -0
- package/openclaw.json.template +56 -0
- package/package.json +39 -4
- package/scripts/entrypoint.sh +496 -0
- package/scripts/patch-openclaw-audio.mjs +158 -0
- package/setup-server/public/index.html +178 -249
- package/setup-server/server.js +251 -58
- package/workspace/skills/google-calendar/SKILL.md +144 -0
- package/workspace/skills/retrieve-file/SKILL.md +41 -0
- package/workspace/system/AGENTS.md +85 -0
- package/workspace/system/IDENTITY.md +61 -0
- package/workspace/system/SOUL.md +71 -0
- package/workspace/system/TOOLS.md +306 -0
- package/workspace/system/limbo-skill.md +152 -0
- package/workspace/templates/USER.md.template +19 -0
- package/.gitlab-ci.yml +0 -208
- package/ARCHITECTURE.md +0 -174
- package/CONTRIBUTING.md +0 -34
- package/SECURITY.md +0 -108
- package/docker-compose.dev.yml +0 -10
- package/docker-compose.test.yml +0 -36
- package/evals/config.eval.env +0 -9
- package/evals/dashboard/public/app.js +0 -975
- package/evals/dashboard/public/index.html +0 -89
- package/evals/dashboard/public/styles.css +0 -908
- package/evals/dashboard/server.js +0 -129
- package/evals/docker-compose.eval.yml +0 -57
- package/evals/promptfoo/assertions.js +0 -215
- package/evals/promptfoo/hooks.js +0 -119
- package/evals/promptfoo/promptfooconfig.yaml +0 -106
- package/evals/promptfoo/provider.js +0 -206
- package/evals/promptfoo/run.sh +0 -25
- package/evals/promptfoo/seeds/notes/eval-seed-birthday.md +0 -9
- package/evals/results/.gitkeep +0 -0
- package/evals/results/history/.gitkeep +0 -0
- package/evals/results/history/run-1774559258082.json +0 -662
- package/evals/results/history/run-1774559485256.json +0 -662
- package/evals/results/history/run-1774559674855.json +0 -662
- package/evals/results/history/run-1774561108314.json +0 -662
- package/evals/results/history/run-1774561286576.json +0 -662
- package/evals/results/history/run-1774561575363.json +0 -575
- package/evals/results/history/run-1774563070869.json +0 -662
- package/evals/results/history/run-1774563275178.json +0 -662
- package/evals/results/history/run-1774622867363.json +0 -934
- package/evals/results/history/run-1774623126438.json +0 -934
- package/evals/results/history/run-1774624683868.json +0 -934
- package/evals/results/history/run-1774625379694.json +0 -934
- package/evals/results/history/run-1774629331960.json +0 -746
- package/evals/results/history/run-1774632319238.json +0 -39
- package/evals/results/history/run-1774633277690.json +0 -94
- package/evals/results/history/run-1774636000952.json +0 -934
- package/evals/results/history/run-1774636946600.json +0 -151
- package/evals/results/history/run-1774637141591.json +0 -374
- package/evals/results/history/run-1774639388611.json +0 -1578
- package/evals/results/history/run-1774641629961.json +0 -1523
- package/evals/results/history/run-1774643063585.json +0 -1653
- package/evals/results/history/run-1774644145726.json +0 -73
- package/evals/results/history/run-1774644299624.json +0 -1489
- package/evals/results/history/run-1774644416754.json +0 -58
- package/evals/results/history/run-1774644909594.json +0 -58
- package/evals/results/history/run-1774796618679.json +0 -73
- package/evals/results/history/run-1774796879800.json +0 -73
- package/evals/results/history/run-1774797434760.json +0 -94
- package/evals/results/history/run-1774797567080.json +0 -57
- package/evals/results/history/run-1774895167606.json +0 -574
- package/evals/results/history/run-1774895670045.json +0 -540
- package/evals/results/history/run-1774895876781.json +0 -466
- package/evals/results/history/run-1774898060232.json +0 -162
- package/evals/results/history/run-1774966775381.json +0 -135
- package/evals/results/history/run-1774966839076.json +0 -33
- package/evals/results/history/run-1774966890459.json +0 -33
- package/evals/results/history/run-1774967730887.json +0 -189
- package/evals/results/history/run-1774967764419.json +0 -113
- package/evals/results/history/run-1775043267611.json +0 -4470
- package/evals/results/history/run-1775046132278.json +0 -4420
- package/evals/results/history/run-1775068115506.json +0 -5277
- package/evals/results/latest.json +0 -5277
- package/evals/test/scorer.test.js +0 -218
- package/mcp-server/test/benchmark.js +0 -365
- package/mcp-server/test/eval-logging.test.js +0 -259
- package/mcp-server/test/get-file.test.js +0 -256
- package/test/cli-auth.test.js +0 -357
- package/test/cli-compose.test.js +0 -471
- package/test/cli-filter.test.js +0 -200
- package/test/cli-registry.test.js +0 -93
- package/test/cli-wizard-parity.test.js +0 -224
- package/test/entrypoint.test.js +0 -776
- package/test/fts.test.js +0 -141
- package/test/mcp-tools.test.js +0 -606
- package/test/openclaw-migration.test.js +0 -317
- package/test/red-phase.test.js +0 -181
- package/test/sanitize-control-chars.test.js +0 -92
- package/test/setup-server.test.js +0 -784
package/Dockerfile
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
# OpenClaw version — pin to avoid surprise upgrades in production
|
|
4
|
+
ARG OPENCLAW_VERSION=latest
|
|
5
|
+
|
|
6
|
+
# ──────────────────────────────────────────────
|
|
7
|
+
# Stage 1: deps — build MCP server native addons
|
|
8
|
+
# better-sqlite3 requires python3/make/g++ for node-gyp compilation.
|
|
9
|
+
# This stage is discarded after extracting node_modules.
|
|
10
|
+
# ──────────────────────────────────────────────
|
|
11
|
+
FROM node:22-slim AS deps
|
|
12
|
+
|
|
13
|
+
# Build tools for native addons (better-sqlite3 requires compilation)
|
|
14
|
+
RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/*
|
|
15
|
+
|
|
16
|
+
WORKDIR /build
|
|
17
|
+
|
|
18
|
+
# Copy MCP server manifest and lockfile (layer cached unless these change)
|
|
19
|
+
COPY mcp-server/package.json mcp-server/package-lock.json* ./mcp-server/
|
|
20
|
+
|
|
21
|
+
# Install deps without running lifecycle scripts, then rebuild better-sqlite3
|
|
22
|
+
# native addon explicitly. Verify with an in-memory open/close smoke test.
|
|
23
|
+
RUN cd mcp-server \
|
|
24
|
+
&& npm ci --omit=dev --ignore-scripts \
|
|
25
|
+
&& cd node_modules/better-sqlite3 \
|
|
26
|
+
&& npx node-gyp rebuild --release \
|
|
27
|
+
&& cd /build \
|
|
28
|
+
&& node -e "const d=require('/build/mcp-server/node_modules/better-sqlite3');const db=d(':memory:');db.close();console.log('better-sqlite3 OK')"
|
|
29
|
+
|
|
30
|
+
# ──────────────────────────────────────────────
|
|
31
|
+
# Stage 2: runtime — OpenClaw + MCP server + workspace
|
|
32
|
+
# Migrated from ZeroClaw (Rust binary) to OpenClaw (Node.js npm package).
|
|
33
|
+
# OpenClaw is installed globally via npm — no custom binary build needed.
|
|
34
|
+
# ──────────────────────────────────────────────
|
|
35
|
+
FROM node:22-slim AS runtime
|
|
36
|
+
|
|
37
|
+
ARG OPENCLAW_VERSION
|
|
38
|
+
|
|
39
|
+
# Runtime system deps:
|
|
40
|
+
# gettext-base — envsubst for config template rendering
|
|
41
|
+
# tzdata — timezone support
|
|
42
|
+
# tini — minimal init for proper signal handling (PID 1 reaping)
|
|
43
|
+
# libssl3 — OpenSSL 3 shared lib needed by OpenClaw's ACP runtime (codex-acp)
|
|
44
|
+
# python3 — required by OpenClaw's pinned-write-helper for safe atomic file writes
|
|
45
|
+
RUN apt-get update && apt-get install -y --no-install-recommends gettext-base tzdata tini libssl3 python3 ca-certificates && rm -rf /var/lib/apt/lists/* \
|
|
46
|
+
&& groupadd -r limbo && useradd --create-home -r -g limbo limbo
|
|
47
|
+
|
|
48
|
+
# Install OpenClaw globally — replaces the ZeroClaw Rust binary.
|
|
49
|
+
# Pinned via OPENCLAW_VERSION build arg (default: latest).
|
|
50
|
+
# @googleworkspace/cli (gws) — Google Calendar integration (optional feature).
|
|
51
|
+
RUN npm install -g "openclaw@${OPENCLAW_VERSION}" "@googleworkspace/cli"
|
|
52
|
+
|
|
53
|
+
# Apply local patch for openclaw#63851 — the guarded fetch drops FormData fields,
|
|
54
|
+
# breaking Groq audio transcription. Remove this once upstream PR #64349 ships in
|
|
55
|
+
# a released openclaw version; the patcher is idempotent and fails loudly if
|
|
56
|
+
# the openclaw code shape has changed.
|
|
57
|
+
COPY scripts/patch-openclaw-audio.mjs /tmp/patch-openclaw-audio.mjs
|
|
58
|
+
RUN node /tmp/patch-openclaw-audio.mjs && rm /tmp/patch-openclaw-audio.mjs
|
|
59
|
+
|
|
60
|
+
# App directories
|
|
61
|
+
WORKDIR /app
|
|
62
|
+
|
|
63
|
+
# MCP server: source code first, then node_modules from deps stage (overrides host binaries)
|
|
64
|
+
COPY --chown=limbo:limbo mcp-server/ ./mcp-server/
|
|
65
|
+
COPY --from=deps /build/mcp-server/node_modules ./mcp-server/node_modules
|
|
66
|
+
|
|
67
|
+
# Setup wizard server (zero dependencies — plain Node.js HTTP server)
|
|
68
|
+
COPY --chown=limbo:limbo setup-server/ /app/setup-server/
|
|
69
|
+
|
|
70
|
+
# System workspace files (product-owned, root-owned for read-only enforcement via symlinks)
|
|
71
|
+
COPY workspace/system/ ./workspace/system/
|
|
72
|
+
|
|
73
|
+
# Skills (product-owned, synced to OpenClaw workspace on boot by entrypoint)
|
|
74
|
+
COPY workspace/skills/ ./workspace/skills/
|
|
75
|
+
|
|
76
|
+
# User workspace templates (limbo-owned, seeded on first run)
|
|
77
|
+
COPY --chown=limbo:limbo workspace/templates/ ./workspace/templates/
|
|
78
|
+
|
|
79
|
+
# Migration runner (no external deps — pure Node.js stdlib)
|
|
80
|
+
COPY --chown=limbo:limbo migrations/ ./migrations/
|
|
81
|
+
|
|
82
|
+
# Shared libs (telegram-notify, wakeup routine)
|
|
83
|
+
COPY --chown=limbo:limbo lib/ ./lib/
|
|
84
|
+
|
|
85
|
+
# Package metadata (version read by wakeup routine)
|
|
86
|
+
COPY --chown=limbo:limbo package.json ./package.json
|
|
87
|
+
|
|
88
|
+
# User-facing release notes (parsed by wakeup routine for update messages)
|
|
89
|
+
COPY --chown=limbo:limbo RELEASES.md ./RELEASES.md
|
|
90
|
+
|
|
91
|
+
# OpenClaw config template (populated by entrypoint from env vars)
|
|
92
|
+
COPY --chown=limbo:limbo openclaw.json.template ./openclaw.json.template
|
|
93
|
+
|
|
94
|
+
# Entrypoint script
|
|
95
|
+
COPY scripts/entrypoint.sh /entrypoint.sh
|
|
96
|
+
RUN chmod +x /entrypoint.sh
|
|
97
|
+
|
|
98
|
+
# Pre-create dirs with correct ownership for image-layer defaults
|
|
99
|
+
RUN mkdir -p /data && chown limbo:limbo /data
|
|
100
|
+
RUN mkdir -p /flags && chown limbo:limbo /flags
|
|
101
|
+
RUN mkdir -p /home/limbo/.openclaw && chown limbo:limbo /home/limbo/.openclaw
|
|
102
|
+
# Fix npm cache ownership — npm install -g runs as root but limbo user needs write access at runtime
|
|
103
|
+
RUN mkdir -p /home/limbo/.npm && chown -R limbo:limbo /home/limbo/.npm
|
|
104
|
+
RUN chown limbo:limbo /app
|
|
105
|
+
|
|
106
|
+
# Data volume — vault, db, config, memory, backups, logs
|
|
107
|
+
VOLUME ["/data"]
|
|
108
|
+
|
|
109
|
+
# OpenClaw gateway port
|
|
110
|
+
EXPOSE 18789
|
|
111
|
+
|
|
112
|
+
# Run as non-root limbo user
|
|
113
|
+
USER limbo
|
|
114
|
+
|
|
115
|
+
# tini as init process for proper signal forwarding and zombie reaping
|
|
116
|
+
ENTRYPOINT ["/usr/bin/tini", "--", "/entrypoint.sh"]
|
package/RELEASES.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Limbo Releases
|
|
2
|
+
|
|
3
|
+
> User-facing changelog. The section for the latest version (above the first `---`)
|
|
4
|
+
> is sent to users as the update notification in Telegram. Keep it non-technical
|
|
5
|
+
> and human-readable. Technical details go below the `---` separator.
|
|
6
|
+
>
|
|
7
|
+
> Format:
|
|
8
|
+
> ```
|
|
9
|
+
> ## vX.Y.Z
|
|
10
|
+
>
|
|
11
|
+
> - Human-readable change 1
|
|
12
|
+
> - Human-readable change 2
|
|
13
|
+
>
|
|
14
|
+
> ---
|
|
15
|
+
>
|
|
16
|
+
> ### Technical changelog
|
|
17
|
+
> - feat: technical description (#PR)
|
|
18
|
+
> - fix: technical description (#PR)
|
|
19
|
+
> ```
|
|
20
|
+
|
|
21
|
+
## v1.30.0
|
|
22
|
+
|
|
23
|
+
- Limbo now notifies you when a new version is available
|
|
24
|
+
- You can update directly from Telegram with one tap
|
|
25
|
+
- Improved startup reliability
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
### Technical changelog
|
|
30
|
+
- feat: update notification system with wakeup routine
|
|
31
|
+
- feat: update_instance MCP tool + flag-file bridge to host
|
|
32
|
+
- feat: telegram-notify lib for deterministic system messages
|
package/cli.js
CHANGED
|
@@ -23,8 +23,10 @@ const LIMBO_DIR = (() => {
|
|
|
23
23
|
})();
|
|
24
24
|
const VAULT_DIR = path.join(LIMBO_DIR, 'vault');
|
|
25
25
|
const OPENCLAW_STATE_DIR = path.join(LIMBO_DIR, 'openclaw-state');
|
|
26
|
+
const FLAGS_DIR = path.join(LIMBO_DIR, 'flags');
|
|
26
27
|
const SECRETS_DIR = path.join(LIMBO_DIR, 'secrets');
|
|
27
|
-
const
|
|
28
|
+
const CONFIG_DIR = path.join(LIMBO_DIR, 'config');
|
|
29
|
+
const ENV_FILE = path.join(CONFIG_DIR, '.env');
|
|
28
30
|
const COMPOSE_FILE = path.join(LIMBO_DIR, 'docker-compose.yml');
|
|
29
31
|
const DEFAULT_REGISTRY = 'registry.gitlab.com/tomas209/limbo';
|
|
30
32
|
const REGISTRY_IMAGE = process.env.LIMBO_REGISTRY || DEFAULT_REGISTRY;
|
|
@@ -184,6 +186,8 @@ function composeContent() {
|
|
|
184
186
|
- limbo-data:/data
|
|
185
187
|
- ${VAULT_DIR}:/data/vault
|
|
186
188
|
- ${OPENCLAW_STATE_DIR}:/home/limbo/.openclaw
|
|
189
|
+
- ${CONFIG_DIR}:/data/config
|
|
190
|
+
- ${FLAGS_DIR}:/flags
|
|
187
191
|
secrets:
|
|
188
192
|
- llm_api_key
|
|
189
193
|
- telegram_bot_token
|
|
@@ -191,14 +195,14 @@ function composeContent() {
|
|
|
191
195
|
- groq_api_key
|
|
192
196
|
- brave_api_key
|
|
193
197
|
env_file:
|
|
194
|
-
- ${
|
|
198
|
+
- ${ENV_FILE}
|
|
195
199
|
environment:
|
|
196
200
|
LIMBO_PORT: "${PORT}"
|
|
197
201
|
NODE_OPTIONS: "\${LIMBO_NODE_OPTIONS:---max-old-space-size=512}"
|
|
198
202
|
${resolveExtraEnv()} healthcheck:
|
|
199
203
|
test:
|
|
200
204
|
- CMD-SHELL
|
|
201
|
-
- node -e "fetch('http://localhost
|
|
205
|
+
- node -e "fetch('http://localhost:${PORT}/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
202
206
|
interval: 10s
|
|
203
207
|
timeout: 5s
|
|
204
208
|
retries: 3
|
|
@@ -246,6 +250,8 @@ function composeContentHardened() {
|
|
|
246
250
|
- limbo-data:/data
|
|
247
251
|
- ${VAULT_DIR}:/data/vault
|
|
248
252
|
- ${OPENCLAW_STATE_DIR}:/home/limbo/.openclaw
|
|
253
|
+
- ${CONFIG_DIR}:/data/config
|
|
254
|
+
- ${FLAGS_DIR}:/flags
|
|
249
255
|
secrets:
|
|
250
256
|
- llm_api_key
|
|
251
257
|
- telegram_bot_token
|
|
@@ -253,7 +259,7 @@ function composeContentHardened() {
|
|
|
253
259
|
- groq_api_key
|
|
254
260
|
- brave_api_key
|
|
255
261
|
env_file:
|
|
256
|
-
- ${
|
|
262
|
+
- ${ENV_FILE}
|
|
257
263
|
environment:
|
|
258
264
|
LIMBO_PORT: "${PORT}"
|
|
259
265
|
NODE_OPTIONS: "\${LIMBO_NODE_OPTIONS:---max-old-space-size=512}"
|
|
@@ -265,7 +271,7 @@ ${resolveExtraEnv()} networks:
|
|
|
265
271
|
healthcheck:
|
|
266
272
|
test:
|
|
267
273
|
- CMD-SHELL
|
|
268
|
-
- node -e "fetch('http://localhost
|
|
274
|
+
- node -e "fetch('http://localhost:${PORT}/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
269
275
|
interval: 10s
|
|
270
276
|
timeout: 5s
|
|
271
277
|
retries: 3
|
|
@@ -762,6 +768,7 @@ function normalizeConfig(cfg, existingEnv = {}) {
|
|
|
762
768
|
GATEWAY_TOKEN: gatewayToken,
|
|
763
769
|
VOICE_ENABLED: cfg.voiceEnabled || existingEnv.VOICE_ENABLED || 'false',
|
|
764
770
|
WEB_SEARCH_ENABLED: cfg.webSearchEnabled || existingEnv.WEB_SEARCH_ENABLED || 'false',
|
|
771
|
+
GOOGLE_CALENDAR_ENABLED: cfg.googleCalendarEnabled || existingEnv.GOOGLE_CALENDAR_ENABLED || 'false',
|
|
765
772
|
};
|
|
766
773
|
|
|
767
774
|
return base;
|
|
@@ -1105,8 +1112,17 @@ function ensureComposeFile(hardened = false) {
|
|
|
1105
1112
|
fs.mkdirSync(path.join(VAULT_DIR, 'notes'), { recursive: true });
|
|
1106
1113
|
fs.mkdirSync(path.join(VAULT_DIR, 'maps'), { recursive: true });
|
|
1107
1114
|
fs.mkdirSync(OPENCLAW_STATE_DIR, { recursive: true });
|
|
1115
|
+
fs.mkdirSync(FLAGS_DIR, { recursive: true });
|
|
1108
1116
|
migrateLegacyState();
|
|
1109
1117
|
fs.mkdirSync(SECRETS_DIR, { recursive: true, mode: 0o700 });
|
|
1118
|
+
// Ensure config dir and .env exist (bind-mounted into container as /data/config/)
|
|
1119
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
1120
|
+
// Migrate legacy .env from LIMBO_DIR root to config/ subdir
|
|
1121
|
+
const legacyEnv = path.join(LIMBO_DIR, '.env');
|
|
1122
|
+
if (legacyEnv !== ENV_FILE && fs.existsSync(legacyEnv) && !fs.existsSync(ENV_FILE)) {
|
|
1123
|
+
fs.renameSync(legacyEnv, ENV_FILE);
|
|
1124
|
+
}
|
|
1125
|
+
if (!fs.existsSync(ENV_FILE)) fs.writeFileSync(ENV_FILE, '', { mode: 0o600 });
|
|
1110
1126
|
// Ensure secret files exist (Docker Compose secrets require the files to be present)
|
|
1111
1127
|
for (const name of ['llm_api_key', 'telegram_bot_token', 'gateway_token', 'groq_api_key', 'brave_api_key']) {
|
|
1112
1128
|
const fp = path.join(SECRETS_DIR, name);
|
|
@@ -2140,19 +2156,22 @@ function cmdUpdate() {
|
|
|
2140
2156
|
return;
|
|
2141
2157
|
}
|
|
2142
2158
|
|
|
2143
|
-
//
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
`image: ${REGISTRY_IMAGE}:${DEFAULT_TAG}`
|
|
2149
|
-
);
|
|
2150
|
-
if (patched !== compose) {
|
|
2151
|
-
compose = patched;
|
|
2152
|
-
fs.writeFileSync(COMPOSE_FILE, compose);
|
|
2153
|
-
log(`Patched compose image to ${REGISTRY_IMAGE}:${DEFAULT_TAG}`);
|
|
2159
|
+
// Restore PORT from existing .env so the regenerated compose uses the right port.
|
|
2160
|
+
const existingEnv = parseEnvFile();
|
|
2161
|
+
if (existingEnv.LIMBO_PORT) {
|
|
2162
|
+
const parsed = parseInt(existingEnv.LIMBO_PORT, 10);
|
|
2163
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= 65535) PORT = parsed;
|
|
2154
2164
|
}
|
|
2155
2165
|
|
|
2166
|
+
// Regenerate compose file from current template. This handles:
|
|
2167
|
+
// - ZeroClaw → OpenClaw migration (volume mounts, healthcheck)
|
|
2168
|
+
// - Image registry/tag updates
|
|
2169
|
+
// - Any new compose changes shipped with the CLI
|
|
2170
|
+
// Detect hardened mode from existing compose (squid sidecar present).
|
|
2171
|
+
const existingCompose = fs.readFileSync(COMPOSE_FILE, 'utf8');
|
|
2172
|
+
const hardened = existingCompose.includes('squid:');
|
|
2173
|
+
ensureComposeFile(hardened);
|
|
2174
|
+
|
|
2156
2175
|
log('Pulling latest image...');
|
|
2157
2176
|
run(`docker compose -f "${COMPOSE_FILE}" pull -q`);
|
|
2158
2177
|
log('Restarting...');
|
|
@@ -2264,6 +2283,122 @@ ${c.bold}Usage:${c.reset}
|
|
|
2264
2283
|
}
|
|
2265
2284
|
}
|
|
2266
2285
|
|
|
2286
|
+
async function cmdSwitchBrain() {
|
|
2287
|
+
const existingEnv = parseEnvFile();
|
|
2288
|
+
if (!existingEnv.MODEL_PROVIDER) {
|
|
2289
|
+
die('No existing configuration found. Run `limbo start` first to set up.');
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// Resolve port from existing config before generating compose file
|
|
2293
|
+
if (existingEnv.LIMBO_PORT) {
|
|
2294
|
+
const parsed = parseInt(existingEnv.LIMBO_PORT, 10);
|
|
2295
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= 65535) PORT = parsed;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
ensureComposeFile(false);
|
|
2299
|
+
|
|
2300
|
+
const lang = existingEnv.CLI_LANGUAGE || 'en';
|
|
2301
|
+
const currentProvider = existingEnv.MODEL_PROVIDER || 'unknown';
|
|
2302
|
+
const currentModel = existingEnv.MODEL_NAME || 'unknown';
|
|
2303
|
+
|
|
2304
|
+
header(lang === 'es' ? 'Cambiar Proveedor' : 'Switch Provider');
|
|
2305
|
+
console.log(` ${c.dim}${lang === 'es' ? 'Proveedor actual' : 'Current provider'}: ${c.reset}${c.bold}${currentProvider}${c.reset} (${currentModel})\n`);
|
|
2306
|
+
|
|
2307
|
+
const envContent = fs.readFileSync(ENV_FILE, 'utf8');
|
|
2308
|
+
const cleaned = envContent
|
|
2309
|
+
.replace(/^SWITCH_BRAIN_MODE=.*\n?/gm, '')
|
|
2310
|
+
.replace(/^AUTH_MODE=.*\n?/gm, '')
|
|
2311
|
+
.replace(/^MODEL_PROVIDER=.*\n?/gm, '')
|
|
2312
|
+
.replace(/^MODEL_NAME=.*\n?/gm, '');
|
|
2313
|
+
fs.writeFileSync(ENV_FILE, cleaned + 'SWITCH_BRAIN_MODE=true\n', { mode: 0o600 });
|
|
2314
|
+
|
|
2315
|
+
pullOrBuildImage(lang);
|
|
2316
|
+
ensureVolumePermissions();
|
|
2317
|
+
|
|
2318
|
+
log(lang === 'es' ? 'Iniciando wizard de cambio de proveedor...' : 'Starting provider switch wizard...');
|
|
2319
|
+
const upResult = runDockerCompose(['up', '-d', '--remove-orphans', '--force-recreate'], { stdio: 'pipe' });
|
|
2320
|
+
if (upResult.status !== 0) {
|
|
2321
|
+
process.stderr.write(upResult.stderr || '');
|
|
2322
|
+
die('Container failed to start. Run `limbo logs` to investigate.');
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
const wizardUrl = extractWizardUrl();
|
|
2326
|
+
|
|
2327
|
+
let tunnel = null;
|
|
2328
|
+
if (isServerEnvironment() || process.argv.includes('--tunnel')) {
|
|
2329
|
+
tunnel = await createSetupTunnel(PORT);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
const displayUrl = wizardUrl || `http://127.0.0.1:${PORT}`;
|
|
2333
|
+
if (!wizardUrl) {
|
|
2334
|
+
warn('Could not extract setup token from container logs.');
|
|
2335
|
+
}
|
|
2336
|
+
printWizardUrl(displayUrl, tunnel);
|
|
2337
|
+
|
|
2338
|
+
try {
|
|
2339
|
+
const envAfter = fs.readFileSync(ENV_FILE, 'utf8');
|
|
2340
|
+
const final = envAfter.replace(/^SWITCH_BRAIN_MODE=.*\n?/gm, '');
|
|
2341
|
+
if (final !== envAfter) fs.writeFileSync(ENV_FILE, final, { mode: 0o600 });
|
|
2342
|
+
} catch {}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
async function cmdConnectCalendar() {
|
|
2346
|
+
const existingEnv = parseEnvFile();
|
|
2347
|
+
if (!existingEnv.MODEL_PROVIDER) {
|
|
2348
|
+
die('No existing configuration found. Run `limbo start` first to set up.');
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
if (existingEnv.GOOGLE_CALENDAR_ENABLED === 'true') {
|
|
2352
|
+
ok('Google Calendar is already connected.');
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// Resolve port from existing config
|
|
2357
|
+
if (existingEnv.LIMBO_PORT) {
|
|
2358
|
+
const parsed = parseInt(existingEnv.LIMBO_PORT, 10);
|
|
2359
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= 65535) PORT = parsed;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
ensureComposeFile(false);
|
|
2363
|
+
|
|
2364
|
+
const lang = existingEnv.CLI_LANGUAGE || 'en';
|
|
2365
|
+
header(lang === 'es' ? 'Conectar Google Calendar' : 'Connect Google Calendar');
|
|
2366
|
+
|
|
2367
|
+
// Write CONNECT_CALENDAR_MODE to .env (preserve existing config)
|
|
2368
|
+
const envContent = fs.readFileSync(ENV_FILE, 'utf8');
|
|
2369
|
+
const cleaned = envContent.replace(/^CONNECT_CALENDAR_MODE=.*\n?/gm, '');
|
|
2370
|
+
fs.writeFileSync(ENV_FILE, cleaned + 'CONNECT_CALENDAR_MODE=true\n', { mode: 0o600 });
|
|
2371
|
+
|
|
2372
|
+
pullOrBuildImage(lang);
|
|
2373
|
+
ensureVolumePermissions();
|
|
2374
|
+
|
|
2375
|
+
log(lang === 'es' ? 'Iniciando wizard de Google Calendar...' : 'Starting Google Calendar setup wizard...');
|
|
2376
|
+
const upResult = runDockerCompose(['up', '-d', '--remove-orphans', '--force-recreate'], { stdio: 'pipe' });
|
|
2377
|
+
if (upResult.status !== 0) {
|
|
2378
|
+
process.stderr.write(upResult.stderr || '');
|
|
2379
|
+
die('Container failed to start. Run `limbo logs` to investigate.');
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
const wizardUrl = extractWizardUrl();
|
|
2383
|
+
|
|
2384
|
+
let tunnel = null;
|
|
2385
|
+
if (isServerEnvironment() || process.argv.includes('--tunnel')) {
|
|
2386
|
+
tunnel = await createSetupTunnel(PORT);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
const displayUrl = wizardUrl || `http://127.0.0.1:${PORT}`;
|
|
2390
|
+
if (!wizardUrl) {
|
|
2391
|
+
warn('Could not extract setup token from container logs.');
|
|
2392
|
+
}
|
|
2393
|
+
printWizardUrl(displayUrl, tunnel);
|
|
2394
|
+
|
|
2395
|
+
try {
|
|
2396
|
+
const envAfter = fs.readFileSync(ENV_FILE, 'utf8');
|
|
2397
|
+
const final = envAfter.replace(/^CONNECT_CALENDAR_MODE=.*\n?/gm, '');
|
|
2398
|
+
if (final !== envAfter) fs.writeFileSync(ENV_FILE, final, { mode: 0o600 });
|
|
2399
|
+
} catch {}
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2267
2402
|
function cmdHelp() {
|
|
2268
2403
|
console.log(`
|
|
2269
2404
|
${c.bold}limbo${c.reset} - personal AI memory agent
|
|
@@ -2277,8 +2412,10 @@ ${c.bold}Commands:${c.reset}
|
|
|
2277
2412
|
logs Tail container logs
|
|
2278
2413
|
update Pull latest image and restart
|
|
2279
2414
|
status Show container status
|
|
2280
|
-
config
|
|
2281
|
-
|
|
2415
|
+
config Configure optional features (voice, web-search)
|
|
2416
|
+
switch-brain Change your AI provider (opens a quick wizard)
|
|
2417
|
+
connect-calendar Connect Google Calendar (opens a quick wizard)
|
|
2418
|
+
help Show this help
|
|
2282
2419
|
|
|
2283
2420
|
${c.bold}Flags:${c.reset}
|
|
2284
2421
|
--cli Use interactive CLI prompts instead of the web setup wizard
|
|
@@ -2402,6 +2539,8 @@ if (require.main === module) {
|
|
|
2402
2539
|
case 'update': cmdUpdate(); break;
|
|
2403
2540
|
case 'status': cmdStatus(); break;
|
|
2404
2541
|
case 'config': cmdConfig(); break;
|
|
2542
|
+
case 'switch-brain': await cmdSwitchBrain(); break;
|
|
2543
|
+
case 'connect-calendar': await cmdConnectCalendar(); break;
|
|
2405
2544
|
case 'version':
|
|
2406
2545
|
case '--version':
|
|
2407
2546
|
case '-v': console.log(require('./package.json').version); break;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* telegram-notify.js — Deterministic Telegram messaging via Bot API.
|
|
3
|
+
*
|
|
4
|
+
* Sends messages directly through the Telegram HTTP API without depending on
|
|
5
|
+
* OpenClaw or any agent runtime. Used by the wakeup routine (entrypoint) and
|
|
6
|
+
* the update_instance MCP tool for system-level notifications that MUST fire.
|
|
7
|
+
*
|
|
8
|
+
* Requires two secrets:
|
|
9
|
+
* - telegram_bot_token
|
|
10
|
+
* - telegram_chat_id
|
|
11
|
+
*
|
|
12
|
+
* Both are written by the setup wizard (setup-server/server.js) during initial
|
|
13
|
+
* Telegram pairing and persisted in the OpenClaw secrets directory.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const https = require("node:https");
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
|
|
20
|
+
const STATE_DIR =
|
|
21
|
+
process.env.OPENCLAW_STATE_DIR || "/home/limbo/.openclaw";
|
|
22
|
+
const SECRETS_DIR = path.join(STATE_DIR, "secrets");
|
|
23
|
+
|
|
24
|
+
function readSecret(name) {
|
|
25
|
+
// Docker secrets take priority, then OpenClaw secrets dir
|
|
26
|
+
for (const dir of ["/run/secrets", SECRETS_DIR]) {
|
|
27
|
+
const p = path.join(dir, name);
|
|
28
|
+
try {
|
|
29
|
+
const v = fs.readFileSync(p, "utf8").trim();
|
|
30
|
+
if (v) return v;
|
|
31
|
+
} catch {
|
|
32
|
+
// not found — try next
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Send a Telegram message via the Bot API.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} text — Message text (supports MarkdownV2 if parse_mode set)
|
|
42
|
+
* @param {object} [options] — Extra sendMessage params (parse_mode, reply_markup, etc.)
|
|
43
|
+
* @returns {Promise<object>} — Telegram API response body
|
|
44
|
+
*/
|
|
45
|
+
function sendMessage(text, options = {}) {
|
|
46
|
+
const token = readSecret("telegram_bot_token");
|
|
47
|
+
const chatId = readSecret("telegram_chat_id");
|
|
48
|
+
|
|
49
|
+
if (!token || !chatId) {
|
|
50
|
+
return Promise.reject(
|
|
51
|
+
new Error("telegram-notify: missing bot_token or chat_id in secrets")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const body = JSON.stringify({
|
|
56
|
+
chat_id: chatId,
|
|
57
|
+
text,
|
|
58
|
+
...options,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const req = https.request(
|
|
63
|
+
{
|
|
64
|
+
hostname: "api.telegram.org",
|
|
65
|
+
path: `/bot${token}/sendMessage`,
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
"Content-Length": Buffer.byteLength(body),
|
|
70
|
+
},
|
|
71
|
+
timeout: 10000,
|
|
72
|
+
},
|
|
73
|
+
(res) => {
|
|
74
|
+
let data = "";
|
|
75
|
+
res.on("data", (chunk) => (data += chunk));
|
|
76
|
+
res.on("end", () => {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(data);
|
|
79
|
+
if (parsed.ok) resolve(parsed);
|
|
80
|
+
else reject(new Error(`Telegram API error: ${data}`));
|
|
81
|
+
} catch {
|
|
82
|
+
reject(new Error(`Telegram API parse error: ${data}`));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
req.on("error", reject);
|
|
88
|
+
req.on("timeout", () => {
|
|
89
|
+
req.destroy();
|
|
90
|
+
reject(new Error("telegram-notify: request timed out"));
|
|
91
|
+
});
|
|
92
|
+
req.write(body);
|
|
93
|
+
req.end();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { sendMessage, readSecret };
|