limbo-ai 1.5.0 → 1.6.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.
Files changed (2) hide show
  1. package/cli.js +53 -71
  2. package/package.json +4 -1
package/cli.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // cli.js — Limbo CLI
3
3
  // Orchestrates the Docker-based Limbo runtime.
4
- // Zero npm dependencies — pure Node.js stdlib.
5
4
  'use strict';
6
5
 
7
6
  const { execSync, spawnSync } = require('child_process');
@@ -14,10 +13,11 @@ const readline = require('readline');
14
13
  // ─── Config ──────────────────────────────────────────────────────────────────
15
14
 
16
15
  const LIMBO_DIR = path.join(os.homedir(), '.limbo');
16
+ const VAULT_DIR = path.join(LIMBO_DIR, 'vault');
17
17
  const ENV_FILE = path.join(LIMBO_DIR, '.env');
18
18
  const COMPOSE_FILE = path.join(LIMBO_DIR, 'docker-compose.yml');
19
19
  const GHCR_IMAGE = 'ghcr.io/tomasward1/limbo';
20
- const DEFAULT_TAG = 'latest';
20
+ const DEFAULT_TAG = require('./package.json').version;
21
21
  const PORT = 18789;
22
22
 
23
23
  // OpenClaw compatibility snapshots from official docs:
@@ -68,6 +68,7 @@ const COMPOSE_CONTENT = `services:
68
68
  - "127.0.0.1:${PORT}:${PORT}"
69
69
  volumes:
70
70
  - limbo-data:/data
71
+ - ./vault:/data/vault
71
72
  - limbo-openclaw-state:/home/limbo/.openclaw
72
73
  env_file:
73
74
  - .env
@@ -278,6 +279,22 @@ function sleep(ms) {
278
279
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
279
280
  }
280
281
 
282
+ let clackPromise;
283
+
284
+ async function getClack() {
285
+ if (!clackPromise) clackPromise = import('@clack/prompts');
286
+ return clackPromise;
287
+ }
288
+
289
+ async function maybeHandleClackCancel(value) {
290
+ const { cancel, isCancel } = await getClack();
291
+ if (isCancel(value)) {
292
+ cancel('Setup cancelled.');
293
+ process.exit(130);
294
+ }
295
+ return value;
296
+ }
297
+
281
298
  function hasDocker() {
282
299
  const result = spawnSync('docker', ['compose', 'version'], { stdio: 'pipe' });
283
300
  return result.status === 0;
@@ -311,25 +328,34 @@ function createPromptInterface() {
311
328
  return readline.createInterface({ input: process.stdin, output: process.stdout });
312
329
  }
313
330
 
314
- async function promptValidated(rl, question, validate, errorMessage) {
331
+ async function promptValidated(question, validate, errorMessage) {
332
+ if (process.stdin.isTTY && process.stdout.isTTY) {
333
+ const { text } = await getClack();
334
+ while (true) {
335
+ const value = await maybeHandleClackCancel(await text({
336
+ message: question.trim(),
337
+ validate: (input) => {
338
+ const validation = validate(String(input ?? ''));
339
+ return validation.ok ? undefined : (validation.message || errorMessage);
340
+ },
341
+ }));
342
+ const validation = validate(String(value));
343
+ if (validation.ok) return validation.value;
344
+ }
345
+ }
346
+
347
+ const rl = createPromptInterface();
315
348
  while (true) {
316
349
  const value = (await prompt(rl, question)).trim();
317
350
  const validation = validate(value);
318
- if (validation.ok) return validation.value;
351
+ if (validation.ok) {
352
+ rl.close();
353
+ return validation.value;
354
+ }
319
355
  warn(validation.message || errorMessage);
320
356
  }
321
357
  }
322
358
 
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
- }
332
-
333
359
  async function selectMenu(question, options, lang) {
334
360
  if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== 'function') {
335
361
  const rl = createPromptInterface();
@@ -345,56 +371,16 @@ async function selectMenu(question, options, lang) {
345
371
  warn('Pick one of the listed options.');
346
372
  }
347
373
  }
348
-
349
- return new Promise((resolve) => {
350
- let selectedIndex = 0;
351
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
352
- readline.emitKeypressEvents(process.stdin, rl);
353
- const previousRawMode = process.stdin.isRaw;
354
- process.stdin.setRawMode(true);
355
-
356
- function cleanup() {
357
- process.stdin.off('keypress', onKeypress);
358
- process.stdin.setRawMode(Boolean(previousRawMode));
359
- rl.close();
360
- process.stdout.write('\n');
361
- }
362
-
363
- function draw() {
364
- readline.cursorTo(process.stdout, 0);
365
- readline.clearScreenDown(process.stdout);
366
- process.stdout.write(`${renderMenu(question, options, selectedIndex, lang)}\n`);
367
- }
368
-
369
- function onKeypress(_, key = {}) {
370
- if (key.name === 'up' || key.name === 'k') {
371
- selectedIndex = selectedIndex === 0 ? options.length - 1 : selectedIndex - 1;
372
- draw();
373
- return;
374
- }
375
-
376
- if (key.name === 'down' || key.name === 'j') {
377
- selectedIndex = selectedIndex === options.length - 1 ? 0 : selectedIndex + 1;
378
- draw();
379
- return;
380
- }
381
-
382
- if (key.name === 'return') {
383
- const value = options[selectedIndex];
384
- cleanup();
385
- resolve(value);
386
- return;
387
- }
388
-
389
- if (key.ctrl && key.name === 'c') {
390
- cleanup();
391
- process.exit(130);
392
- }
393
- }
394
-
395
- process.stdin.on('keypress', onKeypress);
396
- draw();
397
- });
374
+ const { select } = await getClack();
375
+ const selectedValue = await maybeHandleClackCancel(await select({
376
+ message: question,
377
+ options: options.map((option) => ({
378
+ value: option.value,
379
+ label: option.label,
380
+ hint: option.description,
381
+ })),
382
+ }));
383
+ return options.find((option) => option.value === selectedValue) || options[0];
398
384
  }
399
385
 
400
386
  function parseEnvFile() {
@@ -504,14 +490,11 @@ async function collectConfig(existingEnv = {}) {
504
490
 
505
491
  const modelName = await chooseModel(language, providerFamily, accessMethod);
506
492
  const provider = getModelCatalog(providerFamily, accessMethod).provider;
507
-
508
- const rl = createPromptInterface();
509
493
  let apiKey = '';
510
494
 
511
495
  if (accessMethod === 'api-key') {
512
496
  if (providerFamily === 'openai') {
513
497
  apiKey = await promptValidated(
514
- rl,
515
498
  t(language, 'openAiApiKeyPrompt'),
516
499
  (value) => {
517
500
  if (!value) return { ok: false, message: t(language, 'requiredField') };
@@ -521,7 +504,6 @@ async function collectConfig(existingEnv = {}) {
521
504
  );
522
505
  } else {
523
506
  apiKey = await promptValidated(
524
- rl,
525
507
  t(language, 'anthropicApiKeyPrompt'),
526
508
  (value) => {
527
509
  if (!value) return { ok: false, message: t(language, 'requiredField') };
@@ -540,14 +522,11 @@ async function collectConfig(existingEnv = {}) {
540
522
  let telegramToken = '';
541
523
  if (telegramChoice.value === 'true') {
542
524
  telegramToken = await promptValidated(
543
- rl,
544
525
  t(language, 'telegramTokenPrompt'),
545
526
  (value) => value ? { ok: true, value } : { ok: false, message: t(language, 'requiredField') },
546
527
  );
547
528
  }
548
529
 
549
- rl.close();
550
-
551
530
  return {
552
531
  language,
553
532
  authMode: accessMethod,
@@ -563,6 +542,8 @@ async function collectConfig(existingEnv = {}) {
563
542
 
564
543
  function ensureComposeFile() {
565
544
  fs.mkdirSync(LIMBO_DIR, { recursive: true });
545
+ fs.mkdirSync(path.join(VAULT_DIR, 'notes'), { recursive: true });
546
+ fs.mkdirSync(path.join(VAULT_DIR, 'maps'), { recursive: true });
566
547
  fs.writeFileSync(COMPOSE_FILE, COMPOSE_CONTENT);
567
548
  }
568
549
 
@@ -674,6 +655,7 @@ ${c.green}${c.bold}╚═══════════════════
674
655
  ${c.bold}${t(cfg.language, 'gateway')}:${c.reset} ws://127.0.0.1:${PORT}
675
656
  ${c.bold}${t(cfg.language, 'gatewayToken')}:${c.reset} ${gatewayToken}
676
657
  ${c.bold}${t(cfg.language, 'data')}:${c.reset} ${LIMBO_DIR}
658
+ ${c.bold}Vault:${c.reset} ${VAULT_DIR}
677
659
  ${c.bold}${t(cfg.language, 'logs')}:${c.reset} limbo logs
678
660
  ${c.bold}${t(cfg.language, 'stop')}:${c.reset} limbo stop
679
661
  ${c.bold}${t(cfg.language, 'update')}:${c.reset} limbo update
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "limbo-ai",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "Your personal AI memory agent — install and manage Limbo via npx",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -9,6 +9,9 @@
9
9
  "engines": {
10
10
  "node": ">=18"
11
11
  },
12
+ "dependencies": {
13
+ "@clack/prompts": "^1.1.0"
14
+ },
12
15
  "scripts": {
13
16
  "start": "node cli.js start"
14
17
  },