limbo-ai 1.26.0 → 1.28.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/ARCHITECTURE.md +178 -0
- package/README.md +16 -8
- package/assets/og-banner.png +0 -0
- package/cli.js +287 -16
- package/config.toml.template +4 -0
- package/docker-compose.test.yml +5 -0
- package/evals/cases/create-recurring-reminder.json +40 -0
- package/evals/cases/create-reminder.json +29 -8
- package/evals/cases/medium-search-implicit.json +1 -1
- package/evals/cases/reminder-timezone.json +4 -1
- package/evals/cases/search-subdirectory-note.json +24 -0
- package/evals/cases/speed-search-broad.json +14 -0
- package/evals/cases/speed-search-simple.json +14 -0
- package/evals/cases/speed-write-and-search.json +25 -0
- package/evals/cases/telegram-audio.json +19 -0
- package/evals/cases/telegram-pdf.json +21 -0
- package/evals/cases/web-search.json +1 -1
- package/evals/cases/workspace-read-identity.json +14 -0
- package/evals/cases/workspace-write-timezone.json +17 -0
- package/evals/cases/workspace-write-username.json +18 -0
- package/evals/cli.js +622 -73
- package/evals/config.eval.env +8 -0
- package/evals/dashboard/public/app.js +690 -370
- package/evals/dashboard/public/index.html +31 -38
- package/evals/dashboard/public/styles.css +521 -345
- package/evals/dashboard/server.js +22 -14
- package/evals/docker-compose.eval.yml +12 -3
- package/evals/lib/scorer.js +95 -9
- package/evals/lib/vault-diff.js +41 -1
- package/evals/results/baseline.json +928 -101
- package/evals/results/baselines/anthropic__claude-sonnet-4-6__default-full.json +1653 -0
- package/evals/results/baselines/anthropic__claude-sonnet-4-6__medium/search-subdirectory-note.json +140 -0
- package/evals/results/baselines/anthropic__claude-sonnet-4-6__medium-full.json +1489 -0
- package/evals/results/baselines-index.json +38 -0
- package/evals/results/history/run-1774561108314.json +662 -0
- package/evals/results/history/run-1774561286576.json +662 -0
- package/evals/results/history/run-1774561575363.json +575 -0
- package/evals/results/history/run-1774563070869.json +662 -0
- package/evals/results/history/run-1774563275178.json +662 -0
- package/evals/results/history/run-1774622867363.json +934 -0
- package/evals/results/history/run-1774623126438.json +934 -0
- package/evals/results/history/run-1774624683868.json +934 -0
- package/evals/results/history/run-1774625379694.json +934 -0
- package/evals/results/history/run-1774629331960.json +746 -0
- package/evals/results/history/run-1774632319238.json +39 -0
- package/evals/results/history/run-1774633277690.json +94 -0
- package/evals/results/history/run-1774636000952.json +934 -0
- package/evals/results/history/run-1774636946600.json +151 -0
- package/evals/results/history/run-1774637141591.json +374 -0
- package/evals/results/history/run-1774639388611.json +1578 -0
- package/evals/results/history/run-1774641629961.json +1523 -0
- package/evals/results/history/run-1774643063585.json +1653 -0
- package/evals/results/history/run-1774644145726.json +73 -0
- package/evals/results/history/run-1774644299624.json +1489 -0
- package/evals/results/history/run-1774644416754.json +58 -0
- package/evals/results/history/run-1774644909594.json +58 -0
- package/evals/results/history/run-1774796618679.json +73 -0
- package/evals/results/history/run-1774796879800.json +73 -0
- package/evals/results/history/run-1774797434760.json +94 -0
- package/evals/results/history/run-1774797567080.json +57 -0
- package/evals/results/history/run-1774898060232.json +162 -0
- package/evals/results/history/run-1774966775381.json +135 -0
- package/evals/results/history/run-1774966839076.json +33 -0
- package/evals/results/history/run-1774966890459.json +33 -0
- package/evals/results/history/run-1774967730887.json +189 -0
- package/evals/results/history/run-1774967764419.json +113 -0
- package/evals/results/latest.json +116 -616
- package/evals/test/scorer.test.js +38 -0
- package/evals/vault-seed/.README +4 -0
- package/evals/vault-seed/notes/analysis-personal-006.md +10 -0
- package/evals/vault-seed/notes/analysis-personal-016.md +10 -0
- package/evals/vault-seed/notes/analysis-personal-026.md +10 -0
- package/evals/vault-seed/notes/brainstorm-tech-005.md +10 -0
- package/evals/vault-seed/notes/brainstorm-tech-015.md +10 -0
- package/evals/vault-seed/notes/brainstorm-tech-025.md +10 -0
- package/evals/vault-seed/notes/comparison-work-007.md +10 -0
- package/evals/vault-seed/notes/comparison-work-017.md +10 -0
- package/evals/vault-seed/notes/comparison-work-027.md +10 -0
- package/evals/vault-seed/notes/decision-use-postgres.md +10 -0
- package/evals/vault-seed/notes/draft-health-008.md +10 -0
- package/evals/vault-seed/notes/draft-health-018.md +10 -0
- package/evals/vault-seed/notes/draft-health-028.md +10 -0
- package/evals/vault-seed/notes/event-dentist-march.md +10 -0
- package/evals/vault-seed/notes/fact-alergia-mani.md +10 -0
- package/evals/vault-seed/notes/fact-timezone-argentina.md +10 -0
- package/evals/vault-seed/notes/follow-up-personal-001.md +10 -0
- package/evals/vault-seed/notes/follow-up-personal-011.md +10 -0
- package/evals/vault-seed/notes/follow-up-personal-021.md +10 -0
- package/evals/vault-seed/notes/follow-up-personal-031.md +10 -0
- package/evals/vault-seed/notes/idea-whatsapp-agent.md +10 -0
- package/evals/vault-seed/notes/insight-eval-tool-calling.md +10 -0
- package/evals/vault-seed/notes/meeting-tech-000.md +10 -0
- package/evals/vault-seed/notes/meeting-tech-010.md +10 -0
- package/evals/vault-seed/notes/meeting-tech-020.md +10 -0
- package/evals/vault-seed/notes/meeting-tech-030.md +10 -0
- package/evals/vault-seed/notes/newsletter/newsletter-7-ideas.md +11 -0
- package/evals/vault-seed/notes/persona-carlos-ward.md +10 -0
- package/evals/vault-seed/notes/persona-lucas-tech.md +10 -0
- package/evals/vault-seed/notes/persona-maria-lopez.md +10 -0
- package/evals/vault-seed/notes/persona-sofia-globant.md +10 -0
- package/evals/vault-seed/notes/preference-asado-sundays.md +10 -0
- package/evals/vault-seed/notes/project-knok-alerts.md +10 -0
- package/evals/vault-seed/notes/project-limbo-memory-agent.md +10 -0
- package/evals/vault-seed/notes/question-kubernetes-scale.md +10 -0
- package/evals/vault-seed/notes/research-work-002.md +10 -0
- package/evals/vault-seed/notes/research-work-012.md +10 -0
- package/evals/vault-seed/notes/research-work-022.md +10 -0
- package/evals/vault-seed/notes/research-work-032.md +10 -0
- package/evals/vault-seed/notes/review-finance-004.md +10 -0
- package/evals/vault-seed/notes/review-finance-014.md +10 -0
- package/evals/vault-seed/notes/review-finance-024.md +10 -0
- package/evals/vault-seed/notes/review-finance-034.md +10 -0
- package/evals/vault-seed/notes/source-designing-data-intensive.md +10 -0
- package/evals/vault-seed/notes/summary-finance-009.md +10 -0
- package/evals/vault-seed/notes/summary-finance-019.md +10 -0
- package/evals/vault-seed/notes/summary-finance-029.md +10 -0
- package/evals/vault-seed/notes/update-health-003.md +10 -0
- package/evals/vault-seed/notes/update-health-013.md +10 -0
- package/evals/vault-seed/notes/update-health-023.md +10 -0
- package/evals/vault-seed/notes/update-health-033.md +10 -0
- package/mcp-server/fts.js +148 -0
- package/mcp-server/index.js +138 -2
- package/mcp-server/package-lock.json +433 -1
- package/mcp-server/package.json +2 -1
- package/mcp-server/test/eval-logging.test.js +5 -0
- package/mcp-server/tools/get-file.js +74 -0
- package/mcp-server/tools/search.js +3 -7
- package/mcp-server/tools/store-file.js +175 -0
- package/mcp-server/tools/workspace.js +56 -0
- package/mcp-server/tools/write.js +6 -0
- package/mcp-server/vault-index.js +31 -33
- package/package.json +1 -1
- package/setup-server/public/index.html +750 -675
- package/test/fts.test.js +141 -0
- package/test/zeroclaw-migration.test.js +40 -7
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Limbo — Architecture Reference
|
|
2
|
+
|
|
3
|
+
> This file is loaded by AI assistants to avoid re-scanning the codebase every session.
|
|
4
|
+
> Keep it updated when structure changes. Last verified: 2026-03-29.
|
|
5
|
+
|
|
6
|
+
## What Is Limbo
|
|
7
|
+
|
|
8
|
+
Self-hosted personal AI memory agent. Runs as a Docker container exposing a ZeroClaw gateway (WebSocket on :18789). Users interact via Telegram. The agent stores and retrieves knowledge from a markdown vault using MCP tools.
|
|
9
|
+
|
|
10
|
+
**Stack**: ZeroClaw (Rust agent runtime, custom fork) + Node.js MCP server + SQLite FTS5 + Telegram bot.
|
|
11
|
+
|
|
12
|
+
**Published as**: `limbo-ai` on npm — the CLI (`npx limbo-ai`) handles install, start, stop, update, and setup.
|
|
13
|
+
|
|
14
|
+
## High-Level Flow
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
User (Telegram) → ZeroClaw Gateway (:18789) → LLM (configurable provider)
|
|
18
|
+
↓
|
|
19
|
+
MCP Tools (stdio)
|
|
20
|
+
↓
|
|
21
|
+
Vault (markdown + SQLite FTS5)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Directory Structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
limbo/
|
|
28
|
+
├── cli.js # Main CLI (84KB) — install, start, stop, update, configure
|
|
29
|
+
├── Dockerfile # Multi-stage: deps → zeroclaw binary → runtime (node:22-slim)
|
|
30
|
+
├── config.toml.template # ZeroClaw config — rendered by entrypoint via envsubst
|
|
31
|
+
├── docker-compose.yml # Production reference (generated per-user into ~/.limbo)
|
|
32
|
+
├── docker-compose.dev.yml # Local dev
|
|
33
|
+
├── docker-compose.test.yml # Local testing
|
|
34
|
+
├── package.json # npm package: limbo-ai v1.20.4
|
|
35
|
+
│
|
|
36
|
+
├── mcp-server/ # Node.js MCP server (JSON-RPC 2.0 over stdio)
|
|
37
|
+
│ ├── index.js # Entry point — tool routing, vault init, FTS setup
|
|
38
|
+
│ ├── vault-index.js # In-memory vault index (walks markdown files + YAML frontmatter)
|
|
39
|
+
│ ├── fts.js # SQLite FTS5 — BM25 scoring, title-weighted, WAL mode
|
|
40
|
+
│ └── tools/ # One file per MCP tool
|
|
41
|
+
│ ├── search.js # vault_search — FTS5 full-text search
|
|
42
|
+
│ ├── read.js # vault_read — O(1) lookup via in-memory index
|
|
43
|
+
│ ├── write.js # vault_write_note — create/update with YAML frontmatter
|
|
44
|
+
│ ├── update-map.js # vault_update_map — append entries to MOCs
|
|
45
|
+
│ ├── store-file.js # vault_store_file — binary files (images/PDFs) + linked note
|
|
46
|
+
│ └── get-file.js # vault_get_file — retrieve stored files as base64
|
|
47
|
+
│
|
|
48
|
+
├── workspace/ # Agent persona files (injected into ZeroClaw context)
|
|
49
|
+
│ ├── system/ # Product-owned, root-owned, reset every boot
|
|
50
|
+
│ │ ├── AGENTS.md # Behavioral workflows and rules
|
|
51
|
+
│ │ ├── TOOLS.md # Tool usage instructions
|
|
52
|
+
│ │ └── limbo-skill.md # Agent skill definitions
|
|
53
|
+
│ └── templates/ # User-owned, seeded on first run only
|
|
54
|
+
│ ├── IDENTITY.md
|
|
55
|
+
│ ├── SOUL.md
|
|
56
|
+
│ └── USER.md.template # Rendered with envsubst on first run
|
|
57
|
+
│
|
|
58
|
+
├── setup-server/ # Zero-dependency HTTP setup wizard (pure Node.js)
|
|
59
|
+
│ └── server.js # Serves on :18789 until config complete, then exits
|
|
60
|
+
│
|
|
61
|
+
├── migrations/ # Data migration runner
|
|
62
|
+
│ ├── index.js # Runner — executes versioned migrations sequentially
|
|
63
|
+
│ └── versions/ # Individual migration files (4 versions)
|
|
64
|
+
│
|
|
65
|
+
├── scripts/
|
|
66
|
+
│ ├── entrypoint.sh # Container startup (13KB) — 12-stage orchestration
|
|
67
|
+
│ ├── build-zeroclaw.sh # Custom ZeroClaw image builder (multi-platform)
|
|
68
|
+
│ └── install.sh # Server provisioning (Ubuntu/Debian)
|
|
69
|
+
│
|
|
70
|
+
├── evals/ # End-to-end eval framework
|
|
71
|
+
│ ├── cli.js # Eval runner (28KB) — run, compare, promote, judge
|
|
72
|
+
│ ├── docker-compose.eval.yml
|
|
73
|
+
│ ├── cases/ # 20+ JSON test cases (search, create, multi-step, speed)
|
|
74
|
+
│ ├── vault-seed/ # Pre-populated vault for deterministic eval runs
|
|
75
|
+
│ ├── judge/ # LLM-as-judge rubrics
|
|
76
|
+
│ ├── lib/ # Shared eval utilities
|
|
77
|
+
│ ├── dashboard/ # Web UI for results
|
|
78
|
+
│ ├── results/ # Run outputs + baselines/
|
|
79
|
+
│ └── scripts/ # Eval helper scripts
|
|
80
|
+
│
|
|
81
|
+
├── test/ # Unit tests (node --test)
|
|
82
|
+
│ ├── cli-filter.test.js
|
|
83
|
+
│ ├── cli-auth.test.js
|
|
84
|
+
│ ├── zeroclaw-migration.test.js
|
|
85
|
+
│ ├── setup-server.test.js
|
|
86
|
+
│ └── cli-wizard-parity.test.js
|
|
87
|
+
│
|
|
88
|
+
├── docs/ # Public documentation
|
|
89
|
+
├── agents/ # Paperclip agent configs (not deployed in Limbo)
|
|
90
|
+
└── squid/ # Squid proxy config (for container network access)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Docker Build (3 stages)
|
|
94
|
+
|
|
95
|
+
1. **deps** (node:22-slim) — `npm ci` + compile better-sqlite3 native addon
|
|
96
|
+
2. **zeroclaw** — copies binary from custom image `ghcr.io/tomasward1/zeroclaw:<ver>-custom`
|
|
97
|
+
3. **runtime** (node:22-slim) — non-root `limbo` user, copies app + binary + node_modules
|
|
98
|
+
|
|
99
|
+
**Data volume**: `/data` — contains vault/, db/, config/, logs/, backups/, memory/
|
|
100
|
+
|
|
101
|
+
**Build arg**: `ZEROCLAW_IMAGE` — override to test custom ZeroClaw builds locally.
|
|
102
|
+
|
|
103
|
+
## Entrypoint Flow (scripts/entrypoint.sh)
|
|
104
|
+
|
|
105
|
+
12-stage startup:
|
|
106
|
+
1. Directory setup (`/data/*`)
|
|
107
|
+
2. Secrets sync (`/run/secrets/` → `$ZEROCLAW_STATE_DIR/secrets/`)
|
|
108
|
+
3. First-run detection (presence of `.env` in /data)
|
|
109
|
+
4. Setup wizard (if no `MODEL_PROVIDER` in .env → serve wizard on :18789)
|
|
110
|
+
5. Workspace file seeding (templates → /data, system files symlinked)
|
|
111
|
+
6. Config template rendering (envsubst on config.toml.template)
|
|
112
|
+
7. Feature sections (Telegram, Voice, Web Search) conditionally appended to config.toml
|
|
113
|
+
8. Auth profiles generation
|
|
114
|
+
9. Migration runner
|
|
115
|
+
10. FTS index build
|
|
116
|
+
11. MCP server registration
|
|
117
|
+
12. ZeroClaw launch
|
|
118
|
+
|
|
119
|
+
## MCP Server Details
|
|
120
|
+
|
|
121
|
+
- **Protocol**: JSON-RPC 2.0 over stdio
|
|
122
|
+
- **Invoked by ZeroClaw**: `node /app/mcp-server/index.js`
|
|
123
|
+
- **Vault path**: `/data/vault/` (markdown files with YAML frontmatter)
|
|
124
|
+
- **FTS database**: `/data/db/fts.db` (SQLite, WAL mode)
|
|
125
|
+
- **Index**: In-memory hashmap of all vault notes, rebuilt on startup
|
|
126
|
+
|
|
127
|
+
### Frontmatter Schema
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
---
|
|
131
|
+
id: unique-slug
|
|
132
|
+
title: Display Name
|
|
133
|
+
description: Falsifiable claim or summary
|
|
134
|
+
type: note|map|reminder|file
|
|
135
|
+
status: seed|growing|evergreen
|
|
136
|
+
domain: personal|tech|...
|
|
137
|
+
created: 2026-03-29
|
|
138
|
+
source: telegram|manual|...
|
|
139
|
+
topics:
|
|
140
|
+
- "[[related-note]]"
|
|
141
|
+
---
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Key Architectural Decisions
|
|
145
|
+
|
|
146
|
+
These are documented in the vault but rarely change:
|
|
147
|
+
|
|
148
|
+
- **Extension = MCP tools, not ZeroClaw features**. New capabilities go in `mcp-server/tools/` as Node.js. Cargo features only for things that must compile into Rust (e.g., `rag-pdf`).
|
|
149
|
+
- **Separate container, not plugin**. Limbo is a standalone Docker container, not an OpenClaw plugin.
|
|
150
|
+
- **System files reset on boot, user files persist**. AGENTS.md/TOOLS.md overwrite from image; SOUL.md/IDENTITY.md/USER.md survive across container restarts.
|
|
151
|
+
- **Maps live in vault/maps/, notes in vault/notes/**. Separated to simplify `vault_update_map`.
|
|
152
|
+
- **Feature integration pattern**: wizard toggle → secret file → env var → entrypoint appends TOML section.
|
|
153
|
+
- **Minimal .env triggers setup wizard**. Container detects first run by absence of `MODEL_PROVIDER`.
|
|
154
|
+
|
|
155
|
+
## Eval System
|
|
156
|
+
|
|
157
|
+
- 20+ JSON test cases in `evals/cases/`
|
|
158
|
+
- Each case: sends message via WebSocket, asserts on tool_called + response_matches + vault_state
|
|
159
|
+
- Current baseline: 94.0% (FTS5 + ZeroClaw v0.6.3)
|
|
160
|
+
- `node evals/cli.js run` → `compare --strict` → `promote`
|
|
161
|
+
- Uses real LLM calls (costs tokens)
|
|
162
|
+
|
|
163
|
+
## Environment Variables
|
|
164
|
+
|
|
165
|
+
Key env vars (see `.env.example` for full list):
|
|
166
|
+
- `MODEL_PROVIDER` — anthropic, openai, etc.
|
|
167
|
+
- `TELEGRAM_ENABLED` — true/false
|
|
168
|
+
- `LIMBO_PORT` — gateway port (default 18789)
|
|
169
|
+
- `ZEROCLAW_STATE_DIR` — where ZeroClaw stores its state
|
|
170
|
+
- `LIMBO_EVAL` — enables MCP tool call logging
|
|
171
|
+
|
|
172
|
+
## Testing
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npm test # runs: cli-filter, cli-auth, zeroclaw-migration, setup-server, cli-wizard-parity
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Tests use Node.js built-in test runner (`node --test`).
|
package/README.md
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/og-banner.png" alt="Limbo — Tu segundo cerebro" width="720" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/limbo-ai"><img src="https://img.shields.io/npm/v/limbo-ai?color=blue&label=release" alt="npm" /></a>
|
|
7
|
+
<a href="https://github.com/TomasWard1/limbo/actions"><img src="https://img.shields.io/github/actions/workflow/status/TomasWard1/limbo/ci.yml?branch=staging&label=build" alt="build" /></a>
|
|
8
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license" /></a>
|
|
9
|
+
<a href="."><img src="https://img.shields.io/badge/platform-linux%20%7C%20macOS-lightgrey" alt="platform" /></a>
|
|
10
|
+
<a href="https://github.com/TomasWard1/limbo/pkgs/container/limbo"><img src="https://img.shields.io/badge/docker-%E2%9C%93-blue" alt="docker" /></a>
|
|
11
|
+
<a href="https://github.com/TomasWard1/limbo"><img src="https://img.shields.io/github/stars/TomasWard1/limbo?style=social" alt="stars" /></a>
|
|
12
|
+
</p>
|
|
8
13
|
|
|
9
|
-
A personal memory agent
|
|
14
|
+
<p align="center">A personal memory agent that captures ideas, remembers things, and connects knowledge across time.</p>
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
Limbo is a second brain with a conversational interface. It stores atomic notes in a local vault, searches them semantically, and maintains Maps of Content (MOCs) to keep knowledge navigable. Runs in a Docker container, accessible via Telegram or the ZeroClaw gateway.
|
|
12
19
|
|
|
13
20
|
---
|
|
14
21
|
|
|
@@ -208,6 +215,7 @@ Managed by `limbo start`, stored in `~/.limbo/.env`.
|
|
|
208
215
|
| `AUTH_MODE` | `api-key` | `api-key` or `subscription` |
|
|
209
216
|
| `MODEL_PROVIDER` | `anthropic` | `anthropic`, `openai`, `openai-codex`, or `openrouter` |
|
|
210
217
|
| `MODEL_NAME` | `claude-sonnet-4-6` | Model to use |
|
|
218
|
+
| `RUNTIME_REASONING_EFFORT` | `medium` | ZeroClaw `runtime.reasoning_effort` override |
|
|
211
219
|
| `TELEGRAM_ENABLED` | `false` | Enable Telegram integration |
|
|
212
220
|
| `VOICE_ENABLED` | `false` | Enable Groq voice transcription |
|
|
213
221
|
| `WEB_SEARCH_ENABLED` | `false` | Enable Brave web search |
|
|
Binary file
|
package/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ const readline = require('readline');
|
|
|
15
15
|
|
|
16
16
|
const LIMBO_DIR = path.join(os.homedir(), '.limbo');
|
|
17
17
|
const VAULT_DIR = path.join(LIMBO_DIR, 'vault');
|
|
18
|
+
const ZEROCLAW_STATE_DIR = path.join(LIMBO_DIR, 'zeroclaw-state');
|
|
18
19
|
const SECRETS_DIR = path.join(LIMBO_DIR, 'secrets');
|
|
19
20
|
const ENV_FILE = path.join(LIMBO_DIR, '.env');
|
|
20
21
|
const COMPOSE_FILE = path.join(LIMBO_DIR, 'docker-compose.yml');
|
|
@@ -158,7 +159,7 @@ function composeContent() {
|
|
|
158
159
|
volumes:
|
|
159
160
|
- limbo-data:/data
|
|
160
161
|
- ${VAULT_DIR}:/data/vault
|
|
161
|
-
-
|
|
162
|
+
- ${ZEROCLAW_STATE_DIR}:/home/limbo/.zeroclaw
|
|
162
163
|
secrets:
|
|
163
164
|
- llm_api_key
|
|
164
165
|
- telegram_bot_token
|
|
@@ -193,7 +194,6 @@ secrets:
|
|
|
193
194
|
|
|
194
195
|
volumes:
|
|
195
196
|
limbo-data:
|
|
196
|
-
limbo-zeroclaw-state:
|
|
197
197
|
`;
|
|
198
198
|
}
|
|
199
199
|
|
|
@@ -220,7 +220,7 @@ function composeContentHardened() {
|
|
|
220
220
|
volumes:
|
|
221
221
|
- limbo-data:/data
|
|
222
222
|
- ${VAULT_DIR}:/data/vault
|
|
223
|
-
-
|
|
223
|
+
- ${ZEROCLAW_STATE_DIR}:/home/limbo/.zeroclaw
|
|
224
224
|
secrets:
|
|
225
225
|
- llm_api_key
|
|
226
226
|
- telegram_bot_token
|
|
@@ -286,7 +286,6 @@ secrets:
|
|
|
286
286
|
|
|
287
287
|
volumes:
|
|
288
288
|
limbo-data:
|
|
289
|
-
limbo-zeroclaw-state:
|
|
290
289
|
`;
|
|
291
290
|
}
|
|
292
291
|
|
|
@@ -1022,10 +1021,51 @@ async function collectConfig(existingEnv = {}) {
|
|
|
1022
1021
|
};
|
|
1023
1022
|
}
|
|
1024
1023
|
|
|
1024
|
+
// Migrate zeroclaw state from old named volume (limbo_limbo-zeroclaw-state or
|
|
1025
|
+
// limbo-zeroclaw-state) to the new bind-mount directory at ZEROCLAW_STATE_DIR.
|
|
1026
|
+
// Only runs if the bind-mount dir is empty and the named volume exists.
|
|
1027
|
+
function migrateZeroclawStateVolume() {
|
|
1028
|
+
// Skip if bind-mount dir already has content
|
|
1029
|
+
try {
|
|
1030
|
+
const entries = fs.readdirSync(ZEROCLAW_STATE_DIR);
|
|
1031
|
+
if (entries.length > 0) return;
|
|
1032
|
+
} catch { return; }
|
|
1033
|
+
|
|
1034
|
+
// Check whether the old named volume exists (Docker may prefix with project name)
|
|
1035
|
+
const candidateVolumes = ['limbo_limbo-zeroclaw-state', 'limbo-zeroclaw-state'];
|
|
1036
|
+
let foundVolume = null;
|
|
1037
|
+
try {
|
|
1038
|
+
const result = spawnSync('docker', ['volume', 'ls', '--format', '{{.Name}}'], { encoding: 'utf8', stdio: 'pipe' });
|
|
1039
|
+
if (result.status === 0) {
|
|
1040
|
+
const existing = result.stdout.split('\n').map(s => s.trim());
|
|
1041
|
+
foundVolume = candidateVolumes.find(v => existing.includes(v)) || null;
|
|
1042
|
+
}
|
|
1043
|
+
} catch { /* docker not available yet */ }
|
|
1044
|
+
|
|
1045
|
+
if (!foundVolume) return;
|
|
1046
|
+
|
|
1047
|
+
log(`Migrating ZeroClaw state from volume "${foundVolume}" to ${ZEROCLAW_STATE_DIR} ...`);
|
|
1048
|
+
const migrate = spawnSync('docker', [
|
|
1049
|
+
'run', '--rm',
|
|
1050
|
+
'-v', `${foundVolume}:/src:ro`,
|
|
1051
|
+
'-v', `${ZEROCLAW_STATE_DIR}:/dst`,
|
|
1052
|
+
'alpine',
|
|
1053
|
+
'sh', '-c', 'cp -a /src/. /dst/',
|
|
1054
|
+
], { stdio: 'pipe' });
|
|
1055
|
+
|
|
1056
|
+
if (migrate.status === 0) {
|
|
1057
|
+
log('Migration complete. Old volume data is preserved and can be removed with: docker volume rm ' + foundVolume);
|
|
1058
|
+
} else {
|
|
1059
|
+
warn('Migration from old volume failed — continuing with empty state. Run `limbo start` again after verifying Docker is available.');
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1025
1063
|
function ensureComposeFile(hardened = false) {
|
|
1026
1064
|
fs.mkdirSync(LIMBO_DIR, { recursive: true });
|
|
1027
1065
|
fs.mkdirSync(path.join(VAULT_DIR, 'notes'), { recursive: true });
|
|
1028
1066
|
fs.mkdirSync(path.join(VAULT_DIR, 'maps'), { recursive: true });
|
|
1067
|
+
fs.mkdirSync(ZEROCLAW_STATE_DIR, { recursive: true });
|
|
1068
|
+
migrateZeroclawStateVolume();
|
|
1029
1069
|
fs.mkdirSync(SECRETS_DIR, { recursive: true, mode: 0o700 });
|
|
1030
1070
|
// Ensure secret files exist (Docker Compose secrets require the files to be present)
|
|
1031
1071
|
for (const name of ['llm_api_key', 'telegram_bot_token', 'gateway_token', 'groq_api_key', 'brave_api_key']) {
|
|
@@ -1097,24 +1137,191 @@ function ensureVolumePermissions() {
|
|
|
1097
1137
|
], { stdio: 'pipe' });
|
|
1098
1138
|
}
|
|
1099
1139
|
|
|
1100
|
-
// ─── Server detection &
|
|
1140
|
+
// ─── Server detection & tunnel for remote wizard access ─────────────────────
|
|
1101
1141
|
|
|
1102
1142
|
function isServerEnvironment() {
|
|
1103
1143
|
return !!(process.env.SSH_CONNECTION || process.env.SSH_CLIENT ||
|
|
1104
1144
|
(os.platform() === 'linux' && !process.env.DISPLAY));
|
|
1105
1145
|
}
|
|
1106
1146
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1147
|
+
const CF_CERT_PATH = path.join(os.homedir(), '.cloudflared', 'cert.pem');
|
|
1148
|
+
const CF_TUNNEL_CONFIG = path.join(LIMBO_DIR, 'tunnel-config.json');
|
|
1149
|
+
|
|
1150
|
+
function hasCloudflared() {
|
|
1151
|
+
try { execSync('cloudflared --version', { stdio: 'pipe' }); return true; } // hardcoded, safe
|
|
1152
|
+
catch { return false; }
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function isCloudflareLoggedIn() {
|
|
1156
|
+
return fs.existsSync(CF_CERT_PATH);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Interactive prompt: choose tunnel type
|
|
1160
|
+
async function promptTunnelChoice() {
|
|
1161
|
+
const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
|
|
1162
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
1163
|
+
|
|
1164
|
+
console.log(`
|
|
1165
|
+
${c.bold}Setup wizard needs a public URL for your client.${c.reset}
|
|
1166
|
+
|
|
1167
|
+
${c.green}1)${c.reset} Cloudflare tunnel ${c.dim}(stable URL under your domain, recommended)${c.reset}
|
|
1168
|
+
${c.green}2)${c.reset} Quick tunnel ${c.dim}(instant, temporary URL via localhost.run)${c.reset}
|
|
1169
|
+
`);
|
|
1170
|
+
const choice = (await ask(' Choose [1/2]: ')).trim();
|
|
1171
|
+
rl.close();
|
|
1172
|
+
return choice === '2' ? 'quick' : 'cloudflare';
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// cloudflared login (interactive, opens browser or prints URL)
|
|
1176
|
+
async function ensureCloudflareLogin() {
|
|
1177
|
+
if (isCloudflareLoggedIn()) return true;
|
|
1178
|
+
|
|
1179
|
+
log('Logging in to Cloudflare...');
|
|
1180
|
+
log('A browser window will open (or a URL will be printed). Select your domain.\n');
|
|
1181
|
+
|
|
1182
|
+
const result = spawnSync('cloudflared', ['login'], { stdio: 'inherit' });
|
|
1183
|
+
if (result.status !== 0 || !isCloudflareLoggedIn()) {
|
|
1184
|
+
warn('Cloudflare login failed or was cancelled.');
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
ok('Cloudflare login successful.');
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Tunnel hostnames are always setup-<slug>.heylimbo.com
|
|
1192
|
+
const CF_TUNNEL_BASE_DOMAIN = 'heylimbo.com';
|
|
1193
|
+
|
|
1194
|
+
// Create a named CF tunnel using cloudflared CLI (requires cert.pem from login)
|
|
1195
|
+
async function createNamedCfTunnel(port) {
|
|
1196
|
+
const slug = crypto.randomBytes(4).toString('hex').slice(0, 7);
|
|
1197
|
+
const tunnelName = 'limbo-setup-' + slug;
|
|
1198
|
+
const hostname = 'setup-' + slug + '.' + CF_TUNNEL_BASE_DOMAIN;
|
|
1199
|
+
|
|
1200
|
+
try {
|
|
1201
|
+
// 1. Create tunnel
|
|
1202
|
+
spinnerWrite('Creating tunnel...');
|
|
1203
|
+
const createResult = spawnSync('cloudflared', ['tunnel', 'create', tunnelName], {
|
|
1204
|
+
stdio: 'pipe', encoding: 'utf8',
|
|
1205
|
+
});
|
|
1206
|
+
if (createResult.status !== 0) {
|
|
1207
|
+
spinnerClear();
|
|
1208
|
+
warn('Failed to create tunnel: ' + (createResult.stderr || '').trim());
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Extract tunnel ID from output ("Created tunnel <name> with id <uuid>")
|
|
1213
|
+
const idMatch = (createResult.stdout + createResult.stderr).match(/with id ([0-9a-f-]+)/i);
|
|
1214
|
+
if (!idMatch) {
|
|
1215
|
+
spinnerClear();
|
|
1216
|
+
warn('Could not parse tunnel ID from cloudflared output.');
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
const tunnelId = idMatch[1];
|
|
1220
|
+
|
|
1221
|
+
// 2. Route DNS
|
|
1222
|
+
spinnerWrite('Configuring DNS...');
|
|
1223
|
+
const dnsResult = spawnSync('cloudflared', ['tunnel', 'route', 'dns', tunnelName, hostname], {
|
|
1224
|
+
stdio: 'pipe', encoding: 'utf8',
|
|
1225
|
+
});
|
|
1226
|
+
if (dnsResult.status !== 0) {
|
|
1227
|
+
// Non-fatal: might already exist, or we can continue anyway
|
|
1228
|
+
const stderr = (dnsResult.stderr || '').trim();
|
|
1229
|
+
if (!stderr.includes('already exists')) {
|
|
1230
|
+
warn('DNS routing warning: ' + stderr);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// 3. Write minimal config file for this tunnel
|
|
1235
|
+
const cfCredPath = path.join(os.homedir(), '.cloudflared', tunnelId + '.json');
|
|
1236
|
+
const tunnelConfig = path.join(LIMBO_DIR, 'tunnel-cloudflared.yml');
|
|
1237
|
+
const configContent = [
|
|
1238
|
+
'tunnel: ' + tunnelId,
|
|
1239
|
+
'credentials-file: ' + cfCredPath,
|
|
1240
|
+
'ingress:',
|
|
1241
|
+
' - hostname: ' + hostname,
|
|
1242
|
+
' service: http://localhost:' + port,
|
|
1243
|
+
' - service: http_status:404',
|
|
1244
|
+
'',
|
|
1245
|
+
].join('\n');
|
|
1246
|
+
fs.writeFileSync(tunnelConfig, configContent, { mode: 0o600 });
|
|
1247
|
+
|
|
1248
|
+
// 4. Run tunnel
|
|
1249
|
+
const logFile = path.join(LIMBO_DIR, 'tunnel-setup.log');
|
|
1250
|
+
const tunnelProc = spawn('cloudflared', [
|
|
1251
|
+
'tunnel', '--config', tunnelConfig, 'run', tunnelName,
|
|
1252
|
+
], {
|
|
1253
|
+
detached: true,
|
|
1254
|
+
stdio: ['ignore', fs.openSync(logFile, 'w'), fs.openSync(logFile, 'a')],
|
|
1255
|
+
});
|
|
1256
|
+
tunnelProc.unref();
|
|
1257
|
+
|
|
1258
|
+
// Wait for connection
|
|
1259
|
+
let connected = false;
|
|
1260
|
+
for (let i = 0; i < 15; i++) {
|
|
1261
|
+
spinnerWrite('Connecting tunnel...');
|
|
1262
|
+
sleep(1000);
|
|
1263
|
+
try {
|
|
1264
|
+
const logs = fs.readFileSync(logFile, 'utf8');
|
|
1265
|
+
if (logs.includes('Registered tunnel connection') || logs.includes('INF Registered')) {
|
|
1266
|
+
connected = true;
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
} catch {}
|
|
1270
|
+
}
|
|
1271
|
+
spinnerClear();
|
|
1272
|
+
|
|
1273
|
+
if (!connected) {
|
|
1274
|
+
warn('Cloudflare tunnel did not connect in time.');
|
|
1275
|
+
try { tunnelProc.kill(); } catch {}
|
|
1276
|
+
spawnSync('cloudflared', ['tunnel', 'delete', '-f', tunnelName], { stdio: 'pipe' });
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Wait for DNS propagation (Chromium caches negative DNS lookups aggressively)
|
|
1281
|
+
const https = require('https');
|
|
1282
|
+
for (let i = 0; i < 15; i++) {
|
|
1283
|
+
spinnerWrite('Waiting for DNS (' + (i + 1) + 's)...');
|
|
1284
|
+
try {
|
|
1285
|
+
await new Promise((resolve, reject) => {
|
|
1286
|
+
const req = https.get('https://' + hostname + '/healthz', (res) => {
|
|
1287
|
+
resolve(res.statusCode);
|
|
1288
|
+
});
|
|
1289
|
+
req.on('error', reject);
|
|
1290
|
+
req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
|
|
1291
|
+
});
|
|
1292
|
+
break; // DNS resolved and tunnel responded
|
|
1293
|
+
} catch {
|
|
1294
|
+
sleep(1000);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
spinnerClear();
|
|
1298
|
+
|
|
1299
|
+
// Save metadata for cleanup
|
|
1300
|
+
const meta = { tunnelName, tunnelId, hostname, type: 'cloudflare-named' };
|
|
1301
|
+
fs.writeFileSync(CF_TUNNEL_CONFIG, JSON.stringify(meta), { mode: 0o600 });
|
|
1302
|
+
|
|
1303
|
+
return {
|
|
1304
|
+
type: 'cloudflare-named',
|
|
1305
|
+
url: 'https://' + hostname,
|
|
1306
|
+
pid: tunnelProc.pid,
|
|
1307
|
+
logFile,
|
|
1308
|
+
tunnelName,
|
|
1309
|
+
};
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
spinnerClear();
|
|
1312
|
+
warn('Cloudflare tunnel failed: ' + err.message);
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Fallback: localhost.run SSH tunnel (ephemeral, no install needed)
|
|
1318
|
+
async function createQuickTunnel(port) {
|
|
1110
1319
|
try {
|
|
1111
1320
|
const logFile = path.join(LIMBO_DIR, 'tunnel-setup.log');
|
|
1112
|
-
// localhost.run provides instant HTTPS URLs via SSH reverse tunneling.
|
|
1113
|
-
// No binary to install, no DNS propagation delay, no Chrome caching issues.
|
|
1114
1321
|
const tunnelProc = spawn('ssh', [
|
|
1115
1322
|
'-o', 'StrictHostKeyChecking=accept-new',
|
|
1116
1323
|
'-o', 'ServerAliveInterval=30',
|
|
1117
|
-
'-R',
|
|
1324
|
+
'-R', '80:localhost:' + port,
|
|
1118
1325
|
'nokey@localhost.run',
|
|
1119
1326
|
], {
|
|
1120
1327
|
detached: true,
|
|
@@ -1122,7 +1329,6 @@ async function createSetupTunnel(port) {
|
|
|
1122
1329
|
});
|
|
1123
1330
|
tunnelProc.unref();
|
|
1124
1331
|
|
|
1125
|
-
// localhost.run prints the URL almost instantly (no DNS propagation needed)
|
|
1126
1332
|
let tunnelUrl = null;
|
|
1127
1333
|
for (let i = 0; i < 10; i++) {
|
|
1128
1334
|
spinnerWrite('Securing tunnel...');
|
|
@@ -1147,12 +1353,74 @@ async function createSetupTunnel(port) {
|
|
|
1147
1353
|
}
|
|
1148
1354
|
}
|
|
1149
1355
|
|
|
1356
|
+
// Interactive tunnel creation: prompts admin for choice
|
|
1357
|
+
async function createSetupTunnel(port) {
|
|
1358
|
+
const hasCf = hasCloudflared();
|
|
1359
|
+
|
|
1360
|
+
// If cloudflared is available, offer the choice
|
|
1361
|
+
if (hasCf) {
|
|
1362
|
+
const choice = await promptTunnelChoice();
|
|
1363
|
+
|
|
1364
|
+
if (choice === 'cloudflare') {
|
|
1365
|
+
const loggedIn = await ensureCloudflareLogin();
|
|
1366
|
+
if (loggedIn) {
|
|
1367
|
+
const tunnel = await createNamedCfTunnel(port);
|
|
1368
|
+
if (tunnel) return tunnel;
|
|
1369
|
+
warn('Falling back to quick tunnel...');
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
return createQuickTunnel(port);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Clean up tunnel process and CF resources
|
|
1150
1378
|
function teardownSetupTunnel(tunnel) {
|
|
1151
1379
|
if (!tunnel) return;
|
|
1152
1380
|
try { process.kill(tunnel.pid); } catch {}
|
|
1153
1381
|
if (tunnel.logFile) try { fs.unlinkSync(tunnel.logFile); } catch {}
|
|
1154
1382
|
}
|
|
1155
1383
|
|
|
1384
|
+
// Clean up leftover CF tunnels from previous runs
|
|
1385
|
+
function cleanupCfTunnel() {
|
|
1386
|
+
try {
|
|
1387
|
+
const meta = JSON.parse(fs.readFileSync(CF_TUNNEL_CONFIG, 'utf8'));
|
|
1388
|
+
if (meta.tunnelName) {
|
|
1389
|
+
spawnSync('cloudflared', ['tunnel', 'cleanup', meta.tunnelName], { stdio: 'pipe' });
|
|
1390
|
+
spawnSync('cloudflared', ['tunnel', 'delete', '-f', meta.tunnelName], { stdio: 'pipe' });
|
|
1391
|
+
}
|
|
1392
|
+
fs.unlinkSync(CF_TUNNEL_CONFIG);
|
|
1393
|
+
const tunnelConfig = path.join(LIMBO_DIR, 'tunnel-cloudflared.yml');
|
|
1394
|
+
try { fs.unlinkSync(tunnelConfig); } catch {}
|
|
1395
|
+
} catch {}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Read a single env var from ~/.limbo/.env
|
|
1399
|
+
function loadEnvVar(name) {
|
|
1400
|
+
try {
|
|
1401
|
+
const content = fs.readFileSync(ENV_FILE, 'utf8');
|
|
1402
|
+
const match = content.match(new RegExp('^' + name + '=(.+)$', 'm'));
|
|
1403
|
+
return match ? match[1].trim() : null;
|
|
1404
|
+
} catch { return null; }
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Append or update env vars in ~/.limbo/.env without overwriting existing ones
|
|
1408
|
+
function persistEnvVars(vars) {
|
|
1409
|
+
try {
|
|
1410
|
+
let content = '';
|
|
1411
|
+
try { content = fs.readFileSync(ENV_FILE, 'utf8'); } catch {}
|
|
1412
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
1413
|
+
const re = new RegExp('^' + key + '=.*$', 'm');
|
|
1414
|
+
if (re.test(content)) {
|
|
1415
|
+
content = content.replace(re, key + '=' + value);
|
|
1416
|
+
} else {
|
|
1417
|
+
content = content.trimEnd() + '\n' + key + '=' + value + '\n';
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
fs.writeFileSync(ENV_FILE, content, { mode: 0o600 });
|
|
1421
|
+
} catch {}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1156
1424
|
function installGlobalAlias() {
|
|
1157
1425
|
// Create a `limbo` shell wrapper so users don't have to type `npx limbo-ai` every time.
|
|
1158
1426
|
// Tries /usr/local/bin first (macOS, Linux with sudo), falls back to ~/.local/bin (no sudo).
|
|
@@ -1569,6 +1837,9 @@ function writeMinimalEnv() {
|
|
|
1569
1837
|
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
1570
1838
|
|
|
1571
1839
|
async function cmdStart() {
|
|
1840
|
+
// Clean up any leftover CF tunnel from a previous setup run
|
|
1841
|
+
cleanupCfTunnel();
|
|
1842
|
+
|
|
1572
1843
|
// ── Auto-install Docker if missing ────────────────────────────────────────
|
|
1573
1844
|
if (!hasDocker()) {
|
|
1574
1845
|
installDocker();
|
|
@@ -1613,7 +1884,7 @@ async function cmdStart() {
|
|
|
1613
1884
|
const flagApiKey = parseFlag('--api-key');
|
|
1614
1885
|
const flagModel = parseFlag('--model');
|
|
1615
1886
|
const flagLang = parseFlag('--language') || 'en';
|
|
1616
|
-
|
|
1887
|
+
// CF tunnel flags parsed by createSetupTunnel() via parseFlag() — no local var needed
|
|
1617
1888
|
|
|
1618
1889
|
if (flagProvider) {
|
|
1619
1890
|
const validProviders = ['openai', 'anthropic', 'openrouter'];
|
|
@@ -1705,9 +1976,9 @@ async function cmdStart() {
|
|
|
1705
1976
|
// Extract wizard URL from container logs (polls briefly, no healthcheck needed)
|
|
1706
1977
|
const wizardUrl = extractWizardUrl();
|
|
1707
1978
|
|
|
1708
|
-
//
|
|
1979
|
+
// Create a public tunnel (auto on servers, or with --tunnel flag)
|
|
1709
1980
|
let tunnel = null;
|
|
1710
|
-
if (isServerEnvironment()) {
|
|
1981
|
+
if (isServerEnvironment() || process.argv.includes('--tunnel')) {
|
|
1711
1982
|
tunnel = await createSetupTunnel(PORT);
|
|
1712
1983
|
}
|
|
1713
1984
|
|
|
@@ -1970,7 +2241,7 @@ ${c.bold}Flags:${c.reset}
|
|
|
1970
2241
|
--api-key <key> API key for headless install
|
|
1971
2242
|
--model <name> Model name (optional, uses provider default)
|
|
1972
2243
|
--language <code> Language: en, es (default: en)
|
|
1973
|
-
--tunnel
|
|
2244
|
+
--tunnel Force tunnel creation prompt (even on local/non-server environments)
|
|
1974
2245
|
|
|
1975
2246
|
${c.bold}Config:${c.reset}
|
|
1976
2247
|
limbo config voice --enable --api-key gsk_xxx Enable voice transcription
|
package/config.toml.template
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
default_provider = "${MODEL_PROVIDER}"
|
|
6
6
|
default_model = "${MODEL_NAME}"
|
|
7
7
|
|
|
8
|
+
[runtime]
|
|
9
|
+
reasoning_effort = "${RUNTIME_REASONING_EFFORT}"
|
|
10
|
+
|
|
8
11
|
[gateway]
|
|
9
12
|
host = "127.0.0.1"
|
|
10
13
|
port = ${LIMBO_PORT}
|
|
@@ -21,3 +24,4 @@ enabled = true
|
|
|
21
24
|
name = "limbo-vault"
|
|
22
25
|
command = "node"
|
|
23
26
|
args = ["/app/mcp-server/index.js"]
|
|
27
|
+
env = { ZEROCLAW_STATE_DIR = "${ZEROCLAW_STATE_DIR}", ZEROCLAW_WORKSPACE_DIR = "${ZEROCLAW_STATE_DIR}/workspace" }
|