limbo-ai 1.4.0 → 1.6.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.
Files changed (3) hide show
  1. package/README.md +8 -5
  2. package/cli.js +664 -105
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -92,15 +92,18 @@ Managed automatically by `npx limbo-ai start`, stored in `~/.limbo/.env`.
92
92
 
93
93
  | Variable | Required | Default | Description |
94
94
  |----------|----------|---------|-------------|
95
- | `LLM_API_KEY` | **yes*** | | API key for your chosen model provider (Anthropic or OpenAI) |
96
- | `ANTHROPIC_API_KEY` | **yes*** | — | Legacy alias for `LLM_API_KEY` — accepted for backwards compatibility |
97
- | `MODEL_PROVIDER` | no | `anthropic` | Model provider: `anthropic` or `openai` |
98
- | `MODEL_NAME` | no | `claude-sonnet-4-6` | Model name (e.g. `claude-sonnet-4-6`, `codex-mini-latest`, `gpt-4o`) |
95
+ | `AUTH_MODE` | no | `api-key` | `api-key` or `subscription` |
96
+ | `OPENAI_API_KEY` | no* | — | OpenAI API key for `MODEL_PROVIDER=openai` |
97
+ | `ANTHROPIC_API_KEY` | no* | | Anthropic API key for `MODEL_PROVIDER=anthropic` |
98
+ | `LLM_API_KEY` | no | | Legacy generic key path for older installs |
99
+ | `MODEL_PROVIDER` | no | `anthropic` | Model provider: `anthropic`, `openai`, or `openai-codex` |
100
+ | `MODEL_NAME` | no | `claude-opus-4-6` | Model name (e.g. `claude-opus-4-6`, `claude-sonnet-4-6`, `gpt-5.4`) |
99
101
  | `TELEGRAM_ENABLED` | no | `false` | Enable Telegram bot integration |
100
102
  | `TELEGRAM_BOT_TOKEN` | no | — | Telegram bot token (required if `TELEGRAM_ENABLED=true`) |
101
103
  | `TELEGRAM_AUTO_PAIR_FIRST_DM` | no | `true` | Auto-approves the first Telegram DM sender and persists access (MVP-friendly onboarding) |
104
+ | `OPENCLAW_GATEWAY_TOKEN` | no | generated | Stable gateway token for OpenClaw-compatible clients |
102
105
 
103
- > \* Either `LLM_API_KEY` **or** `ANTHROPIC_API_KEY` is required. `LLM_API_KEY` takes precedence if both are set.
106
+ > \* API keys are required only for `AUTH_MODE=api-key`. Subscription auth uses OpenClaw auth profiles instead.
104
107
 
105
108
  ---
106
109
 
package/cli.js CHANGED
@@ -5,10 +5,11 @@
5
5
  'use strict';
6
6
 
7
7
  const { execSync, spawnSync } = require('child_process');
8
+ const crypto = require('crypto');
8
9
  const fs = require('fs');
10
+ const os = require('os');
9
11
  const path = require('path');
10
12
  const readline = require('readline');
11
- const os = require('os');
12
13
 
13
14
  // ─── Config ──────────────────────────────────────────────────────────────────
14
15
 
@@ -16,20 +17,63 @@ const LIMBO_DIR = path.join(os.homedir(), '.limbo');
16
17
  const ENV_FILE = path.join(LIMBO_DIR, '.env');
17
18
  const COMPOSE_FILE = path.join(LIMBO_DIR, 'docker-compose.yml');
18
19
  const GHCR_IMAGE = 'ghcr.io/tomasward1/limbo';
19
- const DEFAULT_TAG = '1.0.0';
20
+ const DEFAULT_TAG = require('./package.json').version;
20
21
  const PORT = 18789;
21
22
 
23
+ // OpenClaw compatibility snapshots from official docs:
24
+ // - https://docs.openclaw.ai/providers/openai
25
+ // - https://docs.openclaw.ai/providers/anthropic
26
+ // - https://docs.openclaw.ai/start/wizard-cli-reference
27
+ const MODEL_CATALOG = {
28
+ 'openai:subscription': {
29
+ provider: 'openai-codex',
30
+ defaultModel: 'gpt-5.4',
31
+ menuModels: ['gpt-5.4'],
32
+ supportedModels: ['gpt-5.4', 'gpt-5.3-codex'],
33
+ },
34
+ 'openai:api-key': {
35
+ provider: 'openai',
36
+ defaultModel: 'gpt-5.4',
37
+ menuModels: ['gpt-5.4', 'gpt-5.4-pro'],
38
+ supportedModels: ['gpt-5.4', 'gpt-5.4-pro', 'gpt-5.1-codex'],
39
+ },
40
+ 'anthropic:subscription': {
41
+ provider: 'anthropic',
42
+ defaultModel: 'claude-opus-4-6',
43
+ menuModels: ['claude-opus-4-6', 'claude-sonnet-4-6'],
44
+ supportedModels: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-opus-4-1', 'claude-sonnet-4'],
45
+ },
46
+ 'anthropic:api-key': {
47
+ provider: 'anthropic',
48
+ defaultModel: 'claude-opus-4-6',
49
+ menuModels: ['claude-opus-4-6', 'claude-sonnet-4-6'],
50
+ supportedModels: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-opus-4-1', 'claude-sonnet-4'],
51
+ },
52
+ };
53
+
54
+ const ASCII_ART = String.raw`
55
+ _ ___ __ __ ____ ___
56
+ | | |_ _| \/ | __ ) / _ \
57
+ | | | || |\/| | _ \| | | |
58
+ | |___ | || | | | |_) | |_| |
59
+ |_____|___|_| |_|____/ \___/
60
+ `;
61
+
22
62
  // docker-compose.yml written to ~/.limbo on install
23
63
  const COMPOSE_CONTENT = `services:
24
64
  limbo:
25
- image: ${GHCR_IMAGE}:\${LIMBO_IMAGE_TAG:-${DEFAULT_TAG}}
65
+ image: ${GHCR_IMAGE}:${DEFAULT_TAG}
26
66
  restart: unless-stopped
27
67
  ports:
28
68
  - "127.0.0.1:${PORT}:${PORT}"
29
69
  volumes:
30
70
  - limbo-data:/data
71
+ - limbo-openclaw-state:/home/limbo/.openclaw
31
72
  env_file:
32
73
  - .env
74
+ environment:
75
+ OPENCLAW_CONFIG_PATH: /home/limbo/.openclaw/openclaw.json
76
+ OPENCLAW_STATE_DIR: /home/limbo/.openclaw
33
77
  healthcheck:
34
78
  test:
35
79
  - CMD-SHELL
@@ -43,27 +87,197 @@ const COMPOSE_CONTENT = `services:
43
87
 
44
88
  volumes:
45
89
  limbo-data:
90
+ limbo-openclaw-state:
46
91
  `;
47
92
 
93
+ const TEXT = {
94
+ en: {
95
+ languageName: 'English',
96
+ chooseLanguage: 'Choose your language',
97
+ menuHelp: 'Use arrow keys and press Enter.',
98
+ providerQuestion: 'AI Provider',
99
+ providerOpenAI: 'Codex (OpenAI)',
100
+ providerAnthropic: 'Claude (Anthropic)',
101
+ accessMethodQuestion: 'Access method',
102
+ accessSubscriptionOpenAI: 'ChatGPT / Codex subscription',
103
+ accessSubscriptionAnthropic: 'Claude Code subscription',
104
+ accessApiKey: 'API token',
105
+ modelQuestion: 'Model',
106
+ customModel: 'Add another supported model name',
107
+ customModelPrompt: ' Model name: ',
108
+ invalidModel: 'That model is not in the current OpenClaw docs allowlist for this provider/auth path.',
109
+ supportedModels: 'Supported models:',
110
+ openAiApiKeyPrompt: ' OpenAI API key (sk-...): ',
111
+ anthropicApiKeyPrompt: ' Anthropic API key (sk-ant-...): ',
112
+ requiredField: 'This field is required.',
113
+ invalidOpenAIKey: 'OpenAI API keys usually start with "sk-".',
114
+ invalidAnthropicKey: 'Anthropic API keys usually start with "sk-ant-".',
115
+ telegramQuestion: 'Want to speak to Limbo through Telegram?',
116
+ telegramTokenPrompt: ' Telegram bot token: ',
117
+ yes: 'Yes',
118
+ no: 'No',
119
+ configuration: 'Configuration',
120
+ reconfiguration: 'Reconfiguration',
121
+ foundExistingConfig: `Found existing config at ${ENV_FILE}`,
122
+ reconfigureHint: 'Starting with existing config. Use --reconfigure to change settings.',
123
+ envWritten: '.env written.',
124
+ pullingImage: 'Pulling image...',
125
+ imagePulled: 'Image pulled.',
126
+ pullFailed: 'Could not pull from GHCR. Trying local build fallback...',
127
+ buildingFallback: 'Building from local Dockerfile...',
128
+ buildOk: (tag) => `Built: ${GHCR_IMAGE}:${tag}`,
129
+ starting: 'Starting Limbo...',
130
+ verifying: 'Verifying health...',
131
+ waitingHealth: (i, max) => `Waiting for container to be healthy... (${i}/${max})`,
132
+ healthTimeout: 'Container did not report healthy within timeout.',
133
+ logsHint: 'Check logs with: limbo logs',
134
+ healthy: 'Container is healthy.',
135
+ subscriptionSetup: 'Provider authentication',
136
+ openaiSubscriptionIntro: 'Limbo will open OpenClaw auth inside the container so you can complete Codex login.',
137
+ anthropicSubscriptionIntro: 'Generate a Claude setup-token on any machine with `claude setup-token`, then paste it into the next step.',
138
+ authFlowStart: 'Starting provider auth flow...',
139
+ authFlowDone: 'Provider auth completed.',
140
+ authFlowFailed: 'Provider auth did not complete successfully.',
141
+ authStatusFailed: 'OpenClaw still reports missing or invalid auth for the selected provider.',
142
+ configFlowStart: 'Applying OpenClaw config...',
143
+ configFlowDone: 'OpenClaw config updated.',
144
+ configFlowFailed: 'Could not update the OpenClaw config for Limbo.',
145
+ success: 'Limbo is running!',
146
+ gateway: 'Gateway',
147
+ gatewayToken: 'Gateway token',
148
+ data: 'Data',
149
+ logs: 'Logs',
150
+ stop: 'Stop',
151
+ update: 'Update',
152
+ telegramEnabledHint: 'Telegram is enabled. Message your bot to start talking to Limbo.',
153
+ nonTelegramHintTitle: 'No Telegram? You can still talk to Limbo through another agent.',
154
+ nonTelegramPromptIntro: 'Suggested prompt:',
155
+ nonTelegramPrompt: (token) => `Connect to my Limbo gateway at ws://127.0.0.1:${PORT} using token ${token}. Use Limbo as my memory layer: save notes, recall context, and update maps of content when I ask.`,
156
+ dockerMissing: 'Docker is not installed or `docker compose` is unavailable.\nInstall Docker Desktop: https://docs.docker.com/get-docker/',
157
+ installMissing: 'Limbo is not installed. Run: npx limbo start',
158
+ helpTitle: 'limbo - personal AI memory agent',
159
+ usage: 'Usage',
160
+ commands: 'Commands',
161
+ flags: 'Flags',
162
+ dataDirectory: 'Data directory',
163
+ helpStart: 'Install and start Limbo (default if no command given)',
164
+ helpStop: 'Stop the running container',
165
+ helpLogs: 'Tail container logs',
166
+ helpUpdate: 'Pull latest image and restart',
167
+ helpStatus: 'Show container status',
168
+ helpHelp: 'Show this help',
169
+ helpReconfigure: 'Reconfigure auth and onboarding settings (use with start)',
170
+ unknownCommand: (cmd) => `Unknown command: ${cmd}`,
171
+ },
172
+ es: {
173
+ languageName: 'Espanol',
174
+ chooseLanguage: 'Elige tu idioma',
175
+ menuHelp: 'Usa las flechas y Enter.',
176
+ providerQuestion: 'AI Provider',
177
+ providerOpenAI: 'Codex (OpenAI)',
178
+ providerAnthropic: 'Claude (Anthropic)',
179
+ accessMethodQuestion: 'Metodo de acceso',
180
+ accessSubscriptionOpenAI: 'Suscripcion ChatGPT / Codex',
181
+ accessSubscriptionAnthropic: 'Suscripcion Claude Code',
182
+ accessApiKey: 'API token',
183
+ modelQuestion: 'Modelo',
184
+ customModel: 'Agregar otro nombre de modelo soportado',
185
+ customModelPrompt: ' Nombre del modelo: ',
186
+ invalidModel: 'Ese modelo no esta en la allowlist actual de OpenClaw para este provider y metodo.',
187
+ supportedModels: 'Modelos soportados:',
188
+ openAiApiKeyPrompt: ' OpenAI API key (sk-...): ',
189
+ anthropicApiKeyPrompt: ' Anthropic API key (sk-ant-...): ',
190
+ requiredField: 'Este campo es obligatorio.',
191
+ invalidOpenAIKey: 'Las API keys de OpenAI normalmente empiezan con "sk-".',
192
+ invalidAnthropicKey: 'Las API keys de Anthropic normalmente empiezan con "sk-ant-".',
193
+ telegramQuestion: 'Quieres hablar con Limbo por Telegram?',
194
+ telegramTokenPrompt: ' Telegram bot token: ',
195
+ yes: 'Si',
196
+ no: 'No',
197
+ configuration: 'Configuracion',
198
+ reconfiguration: 'Reconfiguracion',
199
+ foundExistingConfig: `Se encontro una configuracion existente en ${ENV_FILE}`,
200
+ reconfigureHint: 'Se va a usar la configuracion actual. Usa --reconfigure para cambiarla.',
201
+ envWritten: '.env escrito.',
202
+ pullingImage: 'Bajando imagen...',
203
+ imagePulled: 'Imagen descargada.',
204
+ pullFailed: 'No se pudo bajar la imagen desde GHCR. Probando build local...',
205
+ buildingFallback: 'Construyendo desde el Dockerfile local...',
206
+ buildOk: (tag) => `Imagen construida: ${GHCR_IMAGE}:${tag}`,
207
+ starting: 'Arrancando Limbo...',
208
+ verifying: 'Verificando health...',
209
+ waitingHealth: (i, max) => `Esperando a que el container quede healthy... (${i}/${max})`,
210
+ healthTimeout: 'El container no reporto healthy dentro del timeout.',
211
+ logsHint: 'Mira los logs con: limbo logs',
212
+ healthy: 'El container esta healthy.',
213
+ subscriptionSetup: 'Autenticacion del provider',
214
+ openaiSubscriptionIntro: 'Limbo va a abrir la autenticacion de OpenClaw dentro del container para que completes el login de Codex.',
215
+ anthropicSubscriptionIntro: 'Genera un Claude setup-token en cualquier maquina con `claude setup-token` y pegalo en el siguiente paso.',
216
+ authFlowStart: 'Iniciando autenticacion del provider...',
217
+ authFlowDone: 'Autenticacion del provider completada.',
218
+ authFlowFailed: 'La autenticacion del provider no termino correctamente.',
219
+ authStatusFailed: 'OpenClaw sigue reportando auth faltante o invalida para el provider elegido.',
220
+ configFlowStart: 'Aplicando configuracion de OpenClaw...',
221
+ configFlowDone: 'Configuracion de OpenClaw actualizada.',
222
+ configFlowFailed: 'No se pudo actualizar la configuracion de OpenClaw para Limbo.',
223
+ success: 'Limbo esta corriendo!',
224
+ gateway: 'Gateway',
225
+ gatewayToken: 'Token del gateway',
226
+ data: 'Data',
227
+ logs: 'Logs',
228
+ stop: 'Stop',
229
+ update: 'Update',
230
+ telegramEnabledHint: 'Telegram esta habilitado. Escribile a tu bot para empezar a hablar con Limbo.',
231
+ nonTelegramHintTitle: 'Sin Telegram? Igual puedes hablar con Limbo desde otro agente.',
232
+ nonTelegramPromptIntro: 'Prompt sugerido:',
233
+ nonTelegramPrompt: (token) => `Conectate a mi gateway de Limbo en ws://127.0.0.1:${PORT} usando el token ${token}. Usa Limbo como mi capa de memoria: guarda notas, recupera contexto y actualiza maps of content cuando yo lo pida.`,
234
+ dockerMissing: 'Docker no esta instalado o `docker compose` no esta disponible.\nInstala Docker Desktop: https://docs.docker.com/get-docker/',
235
+ installMissing: 'Limbo no esta instalado. Corre: npx limbo start',
236
+ helpTitle: 'limbo - agente personal de memoria con AI',
237
+ usage: 'Uso',
238
+ commands: 'Comandos',
239
+ flags: 'Flags',
240
+ dataDirectory: 'Directorio de data',
241
+ helpStart: 'Instala y arranca Limbo (default si no pasas comando)',
242
+ helpStop: 'Frena el container',
243
+ helpLogs: 'Sigue los logs del container',
244
+ helpUpdate: 'Baja la ultima imagen y reinicia',
245
+ helpStatus: 'Muestra el estado del container',
246
+ helpHelp: 'Muestra esta ayuda',
247
+ helpReconfigure: 'Reconfigura auth y onboarding (usar con start)',
248
+ unknownCommand: (cmd) => `Comando desconocido: ${cmd}`,
249
+ },
250
+ };
251
+
48
252
  // ─── Colors ──────────────────────────────────────────────────────────────────
49
253
 
50
254
  const c = {
51
255
  reset: '\x1b[0m',
52
- bold: '\x1b[1m',
53
- cyan: '\x1b[36m',
256
+ bold: '\x1b[1m',
257
+ cyan: '\x1b[36m',
54
258
  green: '\x1b[32m',
55
259
  yellow: '\x1b[33m',
56
- red: '\x1b[31m',
260
+ red: '\x1b[31m',
261
+ dim: '\x1b[2m',
57
262
  };
58
263
 
59
- const log = (msg) => console.log(`${c.cyan}[limbo]${c.reset} ${msg}`);
60
- const ok = (msg) => console.log(`${c.green}[limbo]${c.reset} ${msg}`);
61
- const warn = (msg) => console.log(`${c.yellow}[limbo]${c.reset} ${msg}`);
62
- const die = (msg) => { console.error(`${c.red}[limbo] ERROR:${c.reset} ${msg}`); process.exit(1); };
264
+ const log = (msg) => console.log(`${c.cyan}[limbo]${c.reset} ${msg}`);
265
+ const ok = (msg) => console.log(`${c.green}[limbo]${c.reset} ${msg}`);
266
+ const warn = (msg) => console.log(`${c.yellow}[limbo]${c.reset} ${msg}`);
267
+ const die = (msg) => { console.error(`${c.red}[limbo] ERROR:${c.reset} ${msg}`); process.exit(1); };
63
268
  const header = (msg) => console.log(`\n${c.bold}${msg}${c.reset}`);
64
269
 
65
270
  // ─── Helpers ─────────────────────────────────────────────────────────────────
66
271
 
272
+ function t(lang, key, ...args) {
273
+ const value = TEXT[lang][key];
274
+ return typeof value === 'function' ? value(...args) : value;
275
+ }
276
+
277
+ function sleep(ms) {
278
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
279
+ }
280
+
67
281
  function hasDocker() {
68
282
  const result = spawnSync('docker', ['compose', 'version'], { stdio: 'pipe' });
69
283
  return result.status === 0;
@@ -77,161 +291,506 @@ function runQuiet(cmd) {
77
291
  return execSync(cmd, { stdio: 'pipe', cwd: LIMBO_DIR }).toString().trim();
78
292
  }
79
293
 
294
+ function runDockerCompose(args, opts = {}) {
295
+ const result = spawnSync('docker', ['compose', ...args], {
296
+ cwd: LIMBO_DIR,
297
+ stdio: opts.stdio || 'inherit',
298
+ input: opts.input,
299
+ encoding: opts.encoding || 'utf8',
300
+ });
301
+
302
+ if (result.error) throw result.error;
303
+ return result;
304
+ }
305
+
80
306
  function prompt(rl, question) {
81
307
  return new Promise((resolve) => rl.question(question, resolve));
82
308
  }
83
309
 
84
- async function collectConfig() {
85
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
310
+ function createPromptInterface() {
311
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
312
+ }
86
313
 
87
- console.log('Limbo supports Anthropic (Claude) and OpenAI as model providers.');
88
- console.log('Telegram integration is optional — press Enter to skip.\n');
314
+ async function promptValidated(rl, question, validate, errorMessage) {
315
+ while (true) {
316
+ const value = (await prompt(rl, question)).trim();
317
+ const validation = validate(value);
318
+ if (validation.ok) return validation.value;
319
+ warn(validation.message || errorMessage);
320
+ }
321
+ }
89
322
 
90
- const provider = (await prompt(rl, ' Model provider (anthropic/openai) [anthropic]: ')).trim() || 'anthropic';
91
- const isOpenAI = provider === 'openai';
92
- const defaultModel = isOpenAI ? 'codex-mini-latest' : 'claude-sonnet-4-6';
93
- const keyLabel = isOpenAI ? 'OpenAI API key (sk-...)' : 'Anthropic API key (sk-ant-...)';
323
+ function renderMenu(question, options, selectedIndex, lang) {
324
+ const lines = [`${c.bold}${question}${c.reset}`, `${c.dim}${t(lang, 'menuHelp')}${c.reset}`, ''];
325
+ options.forEach((option, index) => {
326
+ const prefix = index === selectedIndex ? `${c.green}>${c.reset}` : ' ';
327
+ lines.push(`${prefix} ${option.label}`);
328
+ if (option.description) lines.push(` ${c.dim}${option.description}${c.reset}`);
329
+ });
330
+ return lines.join('\n');
331
+ }
94
332
 
95
- let llmKey = '';
96
- while (!llmKey) {
97
- llmKey = (await prompt(rl, ` ${keyLabel}: `)).trim();
98
- if (!llmKey) warn('This field is required.');
333
+ async function selectMenu(question, options, lang) {
334
+ if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== 'function') {
335
+ const rl = createPromptInterface();
336
+ while (true) {
337
+ console.log(`\n${question}`);
338
+ options.forEach((option, index) => console.log(` ${index + 1}. ${option.label}`));
339
+ const raw = (await prompt(rl, ' > ')).trim();
340
+ const selected = Number(raw);
341
+ if (Number.isInteger(selected) && selected >= 1 && selected <= options.length) {
342
+ rl.close();
343
+ return options[selected - 1];
344
+ }
345
+ warn('Pick one of the listed options.');
346
+ }
99
347
  }
100
348
 
101
- const modelName = (await prompt(rl, ` Model name [${defaultModel}]: `)).trim() || defaultModel;
102
- const tgRaw = (await prompt(rl, ' Enable Telegram bot? (true/false) [false]: ')).trim() || 'false';
103
- const telegramEnabled = tgRaw === 'true' ? 'true' : 'false';
104
- let telegramToken = '';
105
- if (telegramEnabled === 'true') {
106
- while (!telegramToken) {
107
- telegramToken = (await prompt(rl, ' Telegram bot token: ')).trim();
108
- if (!telegramToken) warn('This field is required when Telegram is enabled.');
349
+ return new Promise((resolve) => {
350
+ let selectedIndex = 0;
351
+ let lastRenderLineCount = 0;
352
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
353
+ readline.emitKeypressEvents(process.stdin, rl);
354
+ const previousRawMode = process.stdin.isRaw;
355
+ process.stdin.setRawMode(true);
356
+
357
+ function cleanup() {
358
+ process.stdin.off('keypress', onKeypress);
359
+ process.stdin.setRawMode(Boolean(previousRawMode));
360
+ rl.close();
361
+ process.stdout.write('\n');
109
362
  }
110
- }
111
363
 
112
- const tag = (await prompt(rl, ` Image tag [${DEFAULT_TAG}]: `)).trim() || DEFAULT_TAG;
364
+ function draw() {
365
+ const output = renderMenu(question, options, selectedIndex, lang);
366
+ if (lastRenderLineCount > 0) {
367
+ readline.moveCursor(process.stdout, 0, -lastRenderLineCount);
368
+ }
369
+ for (let i = 0; i < lastRenderLineCount; i++) {
370
+ readline.clearLine(process.stdout, 0);
371
+ if (i < lastRenderLineCount - 1) readline.moveCursor(process.stdout, 0, 1);
372
+ }
373
+ if (lastRenderLineCount > 0) {
374
+ readline.moveCursor(process.stdout, 0, -Math.max(lastRenderLineCount - 1, 0));
375
+ }
376
+ readline.cursorTo(process.stdout, 0);
377
+ process.stdout.write(output);
378
+ lastRenderLineCount = output.split('\n').length;
379
+ }
113
380
 
114
- rl.close();
115
- return { provider: isOpenAI ? 'openai' : 'anthropic', llmKey, modelName, telegramEnabled, telegramToken, tag };
116
- }
117
-
118
- function writeEnv({ provider, llmKey, modelName, telegramEnabled, telegramToken, tag }) {
119
- const content = [
120
- `LLM_API_KEY=${llmKey}`,
121
- `MODEL_PROVIDER=${provider}`,
122
- `MODEL_NAME=${modelName}`,
123
- `TELEGRAM_ENABLED=${telegramEnabled}`,
124
- `TELEGRAM_BOT_TOKEN=${telegramToken}`,
125
- `LIMBO_IMAGE_TAG=${tag}`,
126
- ].join('\n') + '\n';
381
+ function onKeypress(_, key = {}) {
382
+ if (key.name === 'up' || key.name === 'k') {
383
+ selectedIndex = selectedIndex === 0 ? options.length - 1 : selectedIndex - 1;
384
+ draw();
385
+ return;
386
+ }
387
+
388
+ if (key.name === 'down' || key.name === 'j') {
389
+ selectedIndex = selectedIndex === options.length - 1 ? 0 : selectedIndex + 1;
390
+ draw();
391
+ return;
392
+ }
393
+
394
+ if (key.name === 'return') {
395
+ const value = options[selectedIndex];
396
+ cleanup();
397
+ resolve(value);
398
+ return;
399
+ }
400
+
401
+ if (key.ctrl && key.name === 'c') {
402
+ cleanup();
403
+ process.exit(130);
404
+ }
405
+ }
406
+
407
+ process.stdin.on('keypress', onKeypress);
408
+ draw();
409
+ });
410
+ }
411
+
412
+ function parseEnvFile() {
413
+ if (!fs.existsSync(ENV_FILE)) return {};
414
+ return fs.readFileSync(ENV_FILE, 'utf8')
415
+ .split('\n')
416
+ .filter((line) => line && !line.startsWith('#') && line.includes('='))
417
+ .reduce((acc, line) => {
418
+ const idx = line.indexOf('=');
419
+ acc[line.slice(0, idx)] = line.slice(idx + 1);
420
+ return acc;
421
+ }, {});
422
+ }
423
+
424
+ function generateGatewayToken() {
425
+ return crypto.randomBytes(24).toString('base64url');
426
+ }
427
+
428
+ function normalizeConfig(cfg, existingEnv = {}) {
429
+ const gatewayToken = cfg.gatewayToken || existingEnv.OPENCLAW_GATEWAY_TOKEN || generateGatewayToken();
430
+ const base = {
431
+ CLI_LANGUAGE: cfg.language || existingEnv.CLI_LANGUAGE || 'en',
432
+ AUTH_MODE: cfg.authMode || existingEnv.AUTH_MODE || 'api-key',
433
+ MODEL_PROVIDER: cfg.provider || existingEnv.MODEL_PROVIDER || 'anthropic',
434
+ MODEL_NAME: cfg.modelName || existingEnv.MODEL_NAME || 'claude-opus-4-6',
435
+ OPENAI_API_KEY: cfg.provider === 'openai' && cfg.apiKey ? cfg.apiKey : (cfg.keepExisting ? existingEnv.OPENAI_API_KEY || '' : ''),
436
+ ANTHROPIC_API_KEY: cfg.provider === 'anthropic' && cfg.apiKey ? cfg.apiKey : (cfg.keepExisting ? existingEnv.ANTHROPIC_API_KEY || '' : ''),
437
+ LLM_API_KEY: cfg.apiKey || (cfg.keepExisting ? existingEnv.LLM_API_KEY || '' : ''),
438
+ TELEGRAM_ENABLED: cfg.telegramEnabled || existingEnv.TELEGRAM_ENABLED || 'false',
439
+ TELEGRAM_BOT_TOKEN: cfg.telegramToken || (cfg.keepExisting ? existingEnv.TELEGRAM_BOT_TOKEN || '' : ''),
440
+ TELEGRAM_AUTO_PAIR_FIRST_DM: existingEnv.TELEGRAM_AUTO_PAIR_FIRST_DM || 'true',
441
+ OPENCLAW_GATEWAY_TOKEN: gatewayToken,
442
+ };
443
+
444
+ return base;
445
+ }
446
+
447
+ function writeEnv(cfg, existingEnv = {}) {
448
+ const content = Object.entries(normalizeConfig(cfg, existingEnv))
449
+ .map(([key, value]) => `${key}=${value}`)
450
+ .join('\n') + '\n';
127
451
  fs.writeFileSync(ENV_FILE, content, { mode: 0o600 });
128
452
  }
129
453
 
130
- function waitForHealthy(maxAttempts = 12) {
454
+ function waitForHealthy(lang, maxAttempts = 12) {
131
455
  for (let i = 1; i <= maxAttempts; i++) {
132
456
  try {
133
457
  const raw = runQuiet('docker compose ps --format json');
134
458
  if (raw.includes('"healthy"')) return true;
135
459
  } catch {}
136
- log(`Waiting for container to be healthy... (${i}/${maxAttempts})`);
137
- // simple sync sleep
138
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
460
+ log(t(lang, 'waitingHealth', i, maxAttempts));
461
+ sleep(5000);
139
462
  }
140
463
  return false;
141
464
  }
142
465
 
143
- // ─── Commands ────────────────────────────────────────────────────────────────
466
+ function getModelCatalog(providerFamily, authMode) {
467
+ return MODEL_CATALOG[`${providerFamily}:${authMode}`];
468
+ }
144
469
 
145
- async function cmdStart() {
146
- header('=== Limbo ===');
470
+ async function chooseModel(lang, providerFamily, authMode) {
471
+ const catalog = getModelCatalog(providerFamily, authMode);
472
+ const options = catalog.menuModels.map((model) => ({ label: model, value: model }));
473
+ options.push({ label: t(lang, 'customModel'), value: '__custom__' });
474
+
475
+ const selection = await selectMenu(t(lang, 'modelQuestion'), options, lang);
476
+ if (selection.value !== '__custom__') return selection.value;
147
477
 
148
- if (!hasDocker()) {
149
- die('Docker is not installed or `docker compose` is unavailable.\nInstall Docker Desktop: https://docs.docker.com/get-docker/');
478
+ const rl = createPromptInterface();
479
+ while (true) {
480
+ const modelName = (await prompt(rl, t(lang, 'customModelPrompt'))).trim();
481
+ if (!modelName) {
482
+ warn(t(lang, 'requiredField'));
483
+ continue;
484
+ }
485
+ if (catalog.supportedModels.includes(modelName)) {
486
+ rl.close();
487
+ return modelName;
488
+ }
489
+ warn(t(lang, 'invalidModel'));
490
+ console.log(` ${t(lang, 'supportedModels')} ${catalog.supportedModels.join(', ')}`);
491
+ }
492
+ }
493
+
494
+ async function collectConfig(existingEnv = {}) {
495
+ console.log(`${c.cyan}${ASCII_ART}${c.reset}`);
496
+
497
+ const language = (await selectMenu(t('en', 'chooseLanguage'), [
498
+ { label: TEXT.en.languageName, value: 'en' },
499
+ { label: TEXT.es.languageName, value: 'es' },
500
+ ], 'en')).value;
501
+
502
+ const providerFamily = (await selectMenu(t(language, 'providerQuestion'), [
503
+ { label: t(language, 'providerOpenAI'), value: 'openai' },
504
+ { label: t(language, 'providerAnthropic'), value: 'anthropic' },
505
+ ], language)).value;
506
+
507
+ const accessMethod = (await selectMenu(t(language, 'accessMethodQuestion'), [
508
+ {
509
+ label: providerFamily === 'openai'
510
+ ? t(language, 'accessSubscriptionOpenAI')
511
+ : t(language, 'accessSubscriptionAnthropic'),
512
+ value: 'subscription',
513
+ },
514
+ { label: t(language, 'accessApiKey'), value: 'api-key' },
515
+ ], language)).value;
516
+
517
+ const modelName = await chooseModel(language, providerFamily, accessMethod);
518
+ const provider = getModelCatalog(providerFamily, accessMethod).provider;
519
+
520
+ const rl = createPromptInterface();
521
+ let apiKey = '';
522
+
523
+ if (accessMethod === 'api-key') {
524
+ if (providerFamily === 'openai') {
525
+ apiKey = await promptValidated(
526
+ rl,
527
+ t(language, 'openAiApiKeyPrompt'),
528
+ (value) => {
529
+ if (!value) return { ok: false, message: t(language, 'requiredField') };
530
+ if (!value.startsWith('sk-')) return { ok: false, message: t(language, 'invalidOpenAIKey') };
531
+ return { ok: true, value };
532
+ },
533
+ );
534
+ } else {
535
+ apiKey = await promptValidated(
536
+ rl,
537
+ t(language, 'anthropicApiKeyPrompt'),
538
+ (value) => {
539
+ if (!value) return { ok: false, message: t(language, 'requiredField') };
540
+ if (!value.startsWith('sk-ant-')) return { ok: false, message: t(language, 'invalidAnthropicKey') };
541
+ return { ok: true, value };
542
+ },
543
+ );
544
+ }
150
545
  }
151
546
 
547
+ const telegramChoice = await selectMenu(t(language, 'telegramQuestion'), [
548
+ { label: t(language, 'yes'), value: 'true' },
549
+ { label: t(language, 'no'), value: 'false' },
550
+ ], language);
551
+
552
+ let telegramToken = '';
553
+ if (telegramChoice.value === 'true') {
554
+ telegramToken = await promptValidated(
555
+ rl,
556
+ t(language, 'telegramTokenPrompt'),
557
+ (value) => value ? { ok: true, value } : { ok: false, message: t(language, 'requiredField') },
558
+ );
559
+ }
560
+
561
+ rl.close();
562
+
563
+ return {
564
+ language,
565
+ authMode: accessMethod,
566
+ provider,
567
+ providerFamily,
568
+ modelName,
569
+ apiKey,
570
+ telegramEnabled: telegramChoice.value,
571
+ telegramToken,
572
+ gatewayToken: existingEnv.OPENCLAW_GATEWAY_TOKEN || generateGatewayToken(),
573
+ };
574
+ }
575
+
576
+ function ensureComposeFile() {
152
577
  fs.mkdirSync(LIMBO_DIR, { recursive: true });
153
578
  fs.writeFileSync(COMPOSE_FILE, COMPOSE_CONTENT);
579
+ }
580
+
581
+ function ensureGatewayToken(existingEnv) {
582
+ if (existingEnv.OPENCLAW_GATEWAY_TOKEN) return existingEnv.OPENCLAW_GATEWAY_TOKEN;
583
+ writeEnv({ keepExisting: true }, existingEnv);
584
+ return parseEnvFile().OPENCLAW_GATEWAY_TOKEN;
585
+ }
586
+
587
+ function pullOrBuildImage(lang) {
588
+ header(t(lang, 'pullingImage'));
589
+ try {
590
+ run('docker compose pull -q');
591
+ ok(t(lang, 'imagePulled'));
592
+ } catch {
593
+ warn(t(lang, 'pullFailed'));
594
+ const repoDockerfile = path.join(__dirname, 'Dockerfile');
595
+ if (!fs.existsSync(repoDockerfile)) {
596
+ die('Could not pull image and no local Dockerfile found. Check your network or GHCR access.');
597
+ }
598
+ log(t(lang, 'buildingFallback'));
599
+ execSync(`docker build -t ${GHCR_IMAGE}:${DEFAULT_TAG} .`, { stdio: 'inherit', cwd: __dirname });
600
+ ok(t(lang, 'buildOk', DEFAULT_TAG));
601
+ }
602
+ }
603
+
604
+ function runOpenClaw(args, opts = {}) {
605
+ return runDockerCompose(['run', '--rm', '--entrypoint', 'openclaw', 'limbo', ...args], opts);
606
+ }
607
+
608
+ function applyOpenClawConfig(cfg) {
609
+ header(t(cfg.language, 'configFlowStart'));
610
+
611
+ const setCommands = [
612
+ ['config', 'set', 'gateway.mode', 'local'],
613
+ ['config', 'set', 'gateway.port', String(PORT), '--strict-json'],
614
+ ['config', 'set', 'gateway.bind', 'loopback'],
615
+ ['config', 'set', 'gateway.auth.mode', 'token'],
616
+ ['config', 'set', 'agents.defaults.workspace', '/data/workspace'],
617
+ ['config', 'set', 'agents.defaults.model.primary', `${cfg.provider}/${cfg.modelName}`],
618
+ ];
619
+
620
+ if (cfg.telegramEnabled === 'true') {
621
+ setCommands.push(
622
+ ['config', 'set', 'channels.telegram.enabled', 'true', '--strict-json'],
623
+ ['config', 'set', 'channels.telegram.botToken', cfg.telegramToken],
624
+ );
625
+ }
626
+
627
+ for (const command of setCommands) {
628
+ const result = runOpenClaw(command, { stdio: 'pipe' });
629
+ if (result.status !== 0) {
630
+ process.stdout.write(result.stdout || '');
631
+ process.stderr.write(result.stderr || '');
632
+ die(t(cfg.language, 'configFlowFailed'));
633
+ }
634
+ }
635
+
636
+ if (cfg.telegramEnabled !== 'true') {
637
+ runOpenClaw(['config', 'unset', 'channels.telegram'], { stdio: 'pipe' });
638
+ }
154
639
 
640
+ const validateResult = runOpenClaw(['config', 'validate'], { stdio: 'pipe' });
641
+ if (validateResult.status !== 0) {
642
+ process.stdout.write(validateResult.stdout || '');
643
+ process.stderr.write(validateResult.stderr || '');
644
+ die(t(cfg.language, 'configFlowFailed'));
645
+ }
646
+
647
+ ok(t(cfg.language, 'configFlowDone'));
648
+ }
649
+
650
+ function runSubscriptionAuthFlow(cfg) {
651
+ header(t(cfg.language, 'subscriptionSetup'));
652
+ if (cfg.providerFamily === 'openai') {
653
+ log(t(cfg.language, 'openaiSubscriptionIntro'));
654
+ } else {
655
+ log(t(cfg.language, 'anthropicSubscriptionIntro'));
656
+ }
657
+ log(t(cfg.language, 'authFlowStart'));
658
+
659
+ const authArgs = cfg.providerFamily === 'openai'
660
+ ? ['models', 'auth', 'login', '--provider', 'openai-codex']
661
+ : ['models', 'auth', 'paste-token', '--provider', 'anthropic'];
662
+
663
+ const authResult = runOpenClaw(authArgs);
664
+ if (authResult.status !== 0) die(t(cfg.language, 'authFlowFailed'));
665
+
666
+ const statusResult = runOpenClaw(
667
+ ['models', 'status', '--check', '--probe-provider', cfg.provider],
668
+ { stdio: 'pipe' },
669
+ );
670
+
671
+ if (statusResult.status !== 0) {
672
+ process.stdout.write(statusResult.stdout || '');
673
+ process.stderr.write(statusResult.stderr || '');
674
+ die(t(cfg.language, 'authStatusFailed'));
675
+ }
676
+
677
+ ok(t(cfg.language, 'authFlowDone'));
678
+ }
679
+
680
+ function printSuccess(cfg, gatewayToken) {
681
+ console.log(`
682
+ ${c.green}${c.bold}╔════════════════════════════════════════════╗${c.reset}
683
+ ${c.green}${c.bold}║ ${t(cfg.language, 'success').padEnd(34, ' ')}║${c.reset}
684
+ ${c.green}${c.bold}╚════════════════════════════════════════════╝${c.reset}
685
+
686
+ ${c.bold}${t(cfg.language, 'gateway')}:${c.reset} ws://127.0.0.1:${PORT}
687
+ ${c.bold}${t(cfg.language, 'gatewayToken')}:${c.reset} ${gatewayToken}
688
+ ${c.bold}${t(cfg.language, 'data')}:${c.reset} ${LIMBO_DIR}
689
+ ${c.bold}${t(cfg.language, 'logs')}:${c.reset} limbo logs
690
+ ${c.bold}${t(cfg.language, 'stop')}:${c.reset} limbo stop
691
+ ${c.bold}${t(cfg.language, 'update')}:${c.reset} limbo update
692
+ `);
693
+
694
+ if (cfg.telegramEnabled === 'true') {
695
+ console.log(` ${t(cfg.language, 'telegramEnabledHint')}`);
696
+ return;
697
+ }
698
+
699
+ console.log(` ${t(cfg.language, 'nonTelegramHintTitle')}`);
700
+ console.log(` ${t(cfg.language, 'nonTelegramPromptIntro')}`);
701
+ console.log(` "${t(cfg.language, 'nonTelegramPrompt', gatewayToken)}"`);
702
+ }
703
+
704
+ // ─── Commands ────────────────────────────────────────────────────────────────
705
+
706
+ async function cmdStart() {
707
+ if (!hasDocker()) die(t('en', 'dockerMissing'));
708
+
709
+ ensureComposeFile();
710
+
711
+ const existingEnv = parseEnvFile();
155
712
  const alreadyHasEnv = fs.existsSync(ENV_FILE);
156
713
  let cfg;
714
+ let lang = existingEnv.CLI_LANGUAGE || 'en';
157
715
 
158
716
  if (alreadyHasEnv) {
159
- log(`Found existing config at ${ENV_FILE}`);
717
+ log(existingEnv.MODEL_PROVIDER ? t(lang, 'foundExistingConfig') : `Found existing config at ${ENV_FILE}`);
160
718
  const reconfig = process.argv.includes('--reconfigure');
161
719
  if (!reconfig) {
162
- log('Starting with existing config. Use --reconfigure to change settings.');
163
- cfg = null; // skip writing
720
+ lang = existingEnv.CLI_LANGUAGE || 'en';
721
+ log(t(lang, 'reconfigureHint'));
722
+ ensureGatewayToken(existingEnv);
723
+ cfg = {
724
+ language: lang,
725
+ provider: existingEnv.MODEL_PROVIDER || 'anthropic',
726
+ providerFamily: (existingEnv.MODEL_PROVIDER || 'anthropic').startsWith('openai') ? 'openai' : 'anthropic',
727
+ authMode: existingEnv.AUTH_MODE || 'api-key',
728
+ modelName: existingEnv.MODEL_NAME || 'claude-opus-4-6',
729
+ telegramEnabled: existingEnv.TELEGRAM_ENABLED || 'false',
730
+ };
164
731
  } else {
165
- header('Reconfiguration');
166
- cfg = await collectConfig();
732
+ header(t(lang, 'reconfiguration'));
733
+ cfg = await collectConfig(existingEnv);
734
+ writeEnv({ ...cfg, CLI_LANGUAGE: cfg.language }, existingEnv);
735
+ ok(t(cfg.language, 'envWritten'));
167
736
  }
168
737
  } else {
169
- header('Configuration');
170
- cfg = await collectConfig();
738
+ header(t('en', 'configuration'));
739
+ cfg = await collectConfig(existingEnv);
740
+ writeEnv({ ...cfg, CLI_LANGUAGE: cfg.language }, existingEnv);
741
+ ok(t(cfg.language, 'envWritten'));
171
742
  }
172
743
 
173
- if (cfg) {
174
- writeEnv(cfg);
175
- ok('.env written.');
744
+ const mergedEnv = parseEnvFile();
745
+ if (!cfg.language) cfg.language = mergedEnv.CLI_LANGUAGE || 'en';
746
+ if (!mergedEnv.CLI_LANGUAGE) {
747
+ writeEnv({ ...cfg, keepExisting: true, CLI_LANGUAGE: cfg.language }, mergedEnv);
176
748
  }
177
749
 
178
- header('Pulling image...');
179
- try {
180
- run('docker compose pull -q');
181
- ok('Image pulled.');
182
- } catch {
183
- warn('Could not pull from GHCR. Is the image public? Trying local build fallback...');
184
- // Fallback: build from current directory if we're inside the repo
185
- const repoDockerfile = path.join(__dirname, 'Dockerfile');
186
- if (fs.existsSync(repoDockerfile)) {
187
- log('Building from local Dockerfile...');
188
- const tag = cfg?.tag || DEFAULT_TAG;
189
- execSync(`docker build -t ${GHCR_IMAGE}:${tag} .`, { stdio: 'inherit', cwd: __dirname });
190
- ok(`Built: ${GHCR_IMAGE}:${tag}`);
191
- } else {
192
- die('Could not pull image and no local Dockerfile found. Check your network or GHCR access.');
193
- }
750
+ pullOrBuildImage(cfg.language);
751
+
752
+ if (cfg.authMode === 'subscription' && (process.argv.includes('--reconfigure') || !alreadyHasEnv)) {
753
+ runSubscriptionAuthFlow(cfg);
194
754
  }
195
755
 
196
- header('Starting Limbo...');
756
+ applyOpenClawConfig({
757
+ ...cfg,
758
+ telegramToken: mergedEnv.TELEGRAM_BOT_TOKEN || cfg.telegramToken || '',
759
+ telegramEnabled: mergedEnv.TELEGRAM_ENABLED || cfg.telegramEnabled || 'false',
760
+ });
761
+
762
+ header(t(cfg.language, 'starting'));
197
763
  run('docker compose up -d --remove-orphans');
198
764
 
199
- header('Verifying health...');
200
- const healthy = waitForHealthy();
765
+ header(t(cfg.language, 'verifying'));
766
+ const healthy = waitForHealthy(cfg.language);
201
767
  if (!healthy) {
202
- warn('Container did not report healthy within timeout.');
203
- warn(`Check logs with: limbo logs`);
768
+ warn(t(cfg.language, 'healthTimeout'));
769
+ warn(t(cfg.language, 'logsHint'));
204
770
  } else {
205
- ok('Container is healthy.');
771
+ ok(t(cfg.language, 'healthy'));
206
772
  }
207
773
 
208
- console.log(`
209
- ${c.green}${c.bold}╔════════════════════════════════════════════╗${c.reset}
210
- ${c.green}${c.bold}║ Limbo is running! ║${c.reset}
211
- ${c.green}${c.bold}╚════════════════════════════════════════════╝${c.reset}
212
-
213
- ${c.bold}Gateway:${c.reset} ws://127.0.0.1:${PORT}
214
- ${c.bold}Data:${c.reset} ${LIMBO_DIR}
215
- ${c.bold}Logs:${c.reset} limbo logs
216
- ${c.bold}Stop:${c.reset} limbo stop
217
- ${c.bold}Update:${c.reset} limbo update
218
- `);
774
+ printSuccess({
775
+ language: cfg.language,
776
+ telegramEnabled: mergedEnv.TELEGRAM_ENABLED || cfg.telegramEnabled || 'false',
777
+ }, parseEnvFile().OPENCLAW_GATEWAY_TOKEN);
219
778
  }
220
779
 
221
780
  function cmdStop() {
222
- if (!fs.existsSync(COMPOSE_FILE)) die('Limbo is not installed. Run: npx limbo start');
781
+ if (!fs.existsSync(COMPOSE_FILE)) die(t('en', 'installMissing'));
223
782
  log('Stopping Limbo...');
224
783
  run('docker compose down');
225
784
  ok('Stopped.');
226
785
  }
227
786
 
228
787
  function cmdLogs() {
229
- if (!fs.existsSync(COMPOSE_FILE)) die('Limbo is not installed. Run: npx limbo start');
788
+ if (!fs.existsSync(COMPOSE_FILE)) die(t('en', 'installMissing'));
230
789
  run('docker compose logs -f');
231
790
  }
232
791
 
233
792
  function cmdUpdate() {
234
- if (!fs.existsSync(COMPOSE_FILE)) die('Limbo is not installed. Run: npx limbo start');
793
+ if (!fs.existsSync(COMPOSE_FILE)) die(t('en', 'installMissing'));
235
794
  log('Pulling latest image...');
236
795
  run('docker compose pull -q');
237
796
  log('Restarting...');
@@ -249,7 +808,7 @@ function cmdStatus() {
249
808
 
250
809
  function cmdHelp() {
251
810
  console.log(`
252
- ${c.bold}limbo${c.reset} personal AI memory agent
811
+ ${c.bold}limbo${c.reset} - personal AI memory agent
253
812
 
254
813
  ${c.bold}Usage:${c.reset}
255
814
  npx limbo [command]
@@ -263,7 +822,7 @@ ${c.bold}Commands:${c.reset}
263
822
  help Show this help
264
823
 
265
824
  ${c.bold}Flags:${c.reset}
266
- --reconfigure Reconfigure API keys and settings (use with start)
825
+ --reconfigure Reconfigure auth and onboarding settings (use with start)
267
826
 
268
827
  ${c.bold}Data directory:${c.reset} ${LIMBO_DIR}
269
828
  `);
@@ -277,15 +836,15 @@ const [,, cmd = 'start'] = process.argv;
277
836
  switch (cmd) {
278
837
  case 'start':
279
838
  case 'install': await cmdStart(); break;
280
- case 'stop': cmdStop(); break;
281
- case 'logs': cmdLogs(); break;
839
+ case 'stop': cmdStop(); break;
840
+ case 'logs': cmdLogs(); break;
282
841
  case 'update': cmdUpdate(); break;
283
842
  case 'status': cmdStatus(); break;
284
843
  case 'help':
285
844
  case '--help':
286
845
  case '-h': cmdHelp(); break;
287
846
  default:
288
- warn(`Unknown command: ${cmd}`);
847
+ warn(t('en', 'unknownCommand', cmd));
289
848
  cmdHelp();
290
849
  process.exit(1);
291
850
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "limbo-ai",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Your personal AI memory agent — install and manage Limbo via npx",
5
5
  "type": "commonjs",
6
6
  "bin": {