limbo-ai 1.0.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/.claude/settings.local.json +11 -0
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/README.md +193 -0
- package/cli.js +294 -0
- package/mcp-server/index.js +164 -0
- package/mcp-server/package-lock.json +1144 -0
- package/mcp-server/package.json +16 -0
- package/mcp-server/tools/read.js +30 -0
- package/mcp-server/tools/search.js +85 -0
- package/mcp-server/tools/update-map.js +74 -0
- package/mcp-server/tools/write.js +52 -0
- package/package.json +28 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
> Why do I have a folder named ".vercel" in my project?
|
|
2
|
+
The ".vercel" folder is created when you link a directory to a Vercel project.
|
|
3
|
+
|
|
4
|
+
> What does the "project.json" file contain?
|
|
5
|
+
The "project.json" file contains:
|
|
6
|
+
- The ID of the Vercel project that you linked ("projectId")
|
|
7
|
+
- The ID of the user or team your Vercel project is owned by ("orgId")
|
|
8
|
+
|
|
9
|
+
> Should I commit the ".vercel" folder?
|
|
10
|
+
No, you should not share the ".vercel" folder with anyone.
|
|
11
|
+
Upon creation, it will be automatically added to your ".gitignore" file.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"projectId":"prj_BFmCaecv5QdNrenjJAQy8r6o85ir","orgId":"team_h0ONoWt3JduSqJJuJhnDoDiw","projectName":"limbo-landing"}
|
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Limbo
|
|
2
|
+
|
|
3
|
+
A personal memory agent. Captures ideas, remembers things, and connects knowledge across time — running quietly in a Docker container, accessible via Telegram or the OpenClaw gateway.
|
|
4
|
+
|
|
5
|
+
## What it is
|
|
6
|
+
|
|
7
|
+
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. It is not a general-purpose assistant — it is a memory system.
|
|
8
|
+
|
|
9
|
+
**Agent personality:** defined in `workspace/IDENTITY.md` and `workspace/SOUL.md`, baked into the image at build time.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick Start (Docker Compose)
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
# 1. Copy the env template
|
|
17
|
+
cp .env.example .env
|
|
18
|
+
|
|
19
|
+
# 2. Fill in your credentials (see Environment Variables below)
|
|
20
|
+
$EDITOR .env
|
|
21
|
+
|
|
22
|
+
# 3. Start
|
|
23
|
+
docker compose up -d
|
|
24
|
+
|
|
25
|
+
# 4. Check health
|
|
26
|
+
docker compose ps
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Limbo binds to `127.0.0.1:18789`. Connect via the OpenClaw gateway or Telegram bot.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## One-Line Installer
|
|
34
|
+
|
|
35
|
+
Canonical installer URL:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
https://gist.githubusercontent.com/TomasWard1/d130b8d34cc8eeb0527d045d06985396/raw/install.sh
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Run directly:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
curl -fsSL https://gist.githubusercontent.com/TomasWard1/d130b8d34cc8eeb0527d045d06985396/raw/install.sh | bash
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Run with explicit sudo escalation:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
sudo bash <(curl -fsSL https://gist.githubusercontent.com/TomasWard1/d130b8d34cc8eeb0527d045d06985396/raw/install.sh)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Release Channel (GHCR)
|
|
56
|
+
|
|
57
|
+
Stable deploys should use a pinned semver image tag via `LIMBO_IMAGE_TAG`.
|
|
58
|
+
|
|
59
|
+
- Release workflow source: `.github/workflows/release-ghcr.yml`
|
|
60
|
+
- Published tags per release tag `vX.Y.Z`:
|
|
61
|
+
- `ghcr.io/tomasward1/limbo:X.Y.Z`
|
|
62
|
+
- `ghcr.io/tomasward1/limbo:X`
|
|
63
|
+
- `ghcr.io/tomasward1/limbo:latest`
|
|
64
|
+
|
|
65
|
+
Create a release tag:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
git tag -a v1.0.0 -m "Limbo v1.0.0"
|
|
69
|
+
git push origin v1.0.0
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Verify public pull (no credentials):
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
docker logout ghcr.io
|
|
76
|
+
docker manifest inspect ghcr.io/tomasward1/limbo:1.0.0
|
|
77
|
+
docker pull ghcr.io/tomasward1/limbo:1.0.0
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
If GHCR pull is denied (for example, private package or temporary registry policy), the installer automatically falls back to building from source on the target host.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
## Environment Variables
|
|
84
|
+
|
|
85
|
+
Copy `.env.example` to `.env` and set:
|
|
86
|
+
|
|
87
|
+
| Variable | Required | Default | Description |
|
|
88
|
+
|----------|----------|---------|-------------|
|
|
89
|
+
| `LLM_API_KEY` | **yes*** | — | API key for your chosen model provider (Anthropic or OpenAI) |
|
|
90
|
+
| `ANTHROPIC_API_KEY` | **yes*** | — | Legacy alias for `LLM_API_KEY` — accepted for backwards compatibility |
|
|
91
|
+
| `MODEL_PROVIDER` | no | `anthropic` | Model provider: `anthropic` or `openai` |
|
|
92
|
+
| `MODEL_NAME` | no | `claude-sonnet-4-6` | Model name (e.g. `claude-sonnet-4-6`, `codex-mini-latest`, `gpt-4o`) |
|
|
93
|
+
| `TELEGRAM_ENABLED` | no | `false` | Enable Telegram bot integration |
|
|
94
|
+
| `TELEGRAM_BOT_TOKEN` | no | — | Telegram bot token (required if `TELEGRAM_ENABLED=true`) |
|
|
95
|
+
| `TELEGRAM_AUTO_PAIR_FIRST_DM` | no | `true` | Auto-approves the first Telegram DM sender and persists access (MVP-friendly onboarding) |
|
|
96
|
+
|
|
97
|
+
> \* Either `LLM_API_KEY` **or** `ANTHROPIC_API_KEY` is required. `LLM_API_KEY` takes precedence if both are set.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## MCP Tools
|
|
102
|
+
|
|
103
|
+
Limbo exposes 4 tools via the `limbo-vault` MCP server:
|
|
104
|
+
|
|
105
|
+
| Tool | Description |
|
|
106
|
+
|------|-------------|
|
|
107
|
+
| `vault_search` | Search notes by regex or keyword |
|
|
108
|
+
| `vault_read` | Read a note by ID (returns raw markdown + frontmatter) |
|
|
109
|
+
| `vault_write_note` | Create or overwrite a note with structured frontmatter |
|
|
110
|
+
| `vault_update_map` | Append entries to a Map of Content (MOC) |
|
|
111
|
+
|
|
112
|
+
Full tool specs in `workspace/TOOLS.md`.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Architecture
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
┌─────────────────────────────────────────┐
|
|
120
|
+
│ Docker Container │
|
|
121
|
+
│ │
|
|
122
|
+
│ ┌─────────────┐ ┌────────────────┐ │
|
|
123
|
+
│ │ OpenClaw │◄──►│ Claude (LLM) │ │
|
|
124
|
+
│ │ Gateway │ └────────┬───────┘ │
|
|
125
|
+
│ │ :18789 │ │ │
|
|
126
|
+
│ └──────┬──────┘ ┌────────▼───────┐ │
|
|
127
|
+
│ │ │ MCP Server │ │
|
|
128
|
+
│ Telegram Bot │ limbo-vault │ │
|
|
129
|
+
│ │ └────────┬───────┘ │
|
|
130
|
+
│ └────────────────────┤ │
|
|
131
|
+
│ ▼ │
|
|
132
|
+
│ /data/vault/ │
|
|
133
|
+
│ (markdown notes) │
|
|
134
|
+
└─────────────────────────────────────────┘
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- **OpenClaw** — gateway that wraps the Claude API with MCP tool support and optional Telegram integration
|
|
138
|
+
- **MCP server** — Node.js server providing vault read/write tools
|
|
139
|
+
- **Vault** — plain markdown files with YAML frontmatter, persisted in a named Docker volume
|
|
140
|
+
- **Migrations** — lightweight Node.js migration runner for vault schema changes
|
|
141
|
+
|
|
142
|
+
**Data directory layout** (in `/data` volume):
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
/data/
|
|
146
|
+
vault/ # markdown notes
|
|
147
|
+
db/ # sqlite (future use)
|
|
148
|
+
logs/ # startup and runtime logs
|
|
149
|
+
backups/ # snapshots
|
|
150
|
+
memory/ # agent memory
|
|
151
|
+
config/
|
|
152
|
+
USER.md # per-user persona file (generated at runtime)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Development Setup
|
|
158
|
+
|
|
159
|
+
### Prerequisites
|
|
160
|
+
|
|
161
|
+
- Docker + Docker Compose
|
|
162
|
+
- Node.js 22+ (for local MCP server dev)
|
|
163
|
+
|
|
164
|
+
### Run MCP server locally
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
cd mcp-server
|
|
168
|
+
npm install
|
|
169
|
+
VAULT_PATH=./dev-vault node index.js
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Build image locally
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
docker build -t limbo:dev .
|
|
176
|
+
docker run --rm -e LLM_API_KEY=sk-ant-... -p 18789:18789 limbo:dev
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Run migrations standalone
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
node migrations/index.js
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Connecting
|
|
188
|
+
|
|
189
|
+
**Via OpenClaw (direct):**
|
|
190
|
+
Point any OpenClaw-compatible client at `ws://localhost:18789`.
|
|
191
|
+
|
|
192
|
+
**Via Telegram:**
|
|
193
|
+
Set `TELEGRAM_ENABLED=true` and `TELEGRAM_BOT_TOKEN` in `.env`, then message your bot.
|
package/cli.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cli.js — Limbo CLI
|
|
3
|
+
// Orchestrates the Docker-based Limbo runtime.
|
|
4
|
+
// Zero npm dependencies — pure Node.js stdlib.
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { execSync, spawnSync } = require('child_process');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const readline = require('readline');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const LIMBO_DIR = path.join(os.homedir(), '.limbo');
|
|
16
|
+
const ENV_FILE = path.join(LIMBO_DIR, '.env');
|
|
17
|
+
const COMPOSE_FILE = path.join(LIMBO_DIR, 'docker-compose.yml');
|
|
18
|
+
const GHCR_IMAGE = 'ghcr.io/tomasward1/limbo';
|
|
19
|
+
const DEFAULT_TAG = '1.0.0';
|
|
20
|
+
const PORT = 18789;
|
|
21
|
+
|
|
22
|
+
// docker-compose.yml written to ~/.limbo on install
|
|
23
|
+
const COMPOSE_CONTENT = `services:
|
|
24
|
+
limbo:
|
|
25
|
+
image: ${GHCR_IMAGE}:\${LIMBO_IMAGE_TAG:-${DEFAULT_TAG}}
|
|
26
|
+
restart: unless-stopped
|
|
27
|
+
ports:
|
|
28
|
+
- "127.0.0.1:${PORT}:${PORT}"
|
|
29
|
+
volumes:
|
|
30
|
+
- limbo-data:/data
|
|
31
|
+
env_file:
|
|
32
|
+
- .env
|
|
33
|
+
healthcheck:
|
|
34
|
+
test:
|
|
35
|
+
- CMD-SHELL
|
|
36
|
+
- >-
|
|
37
|
+
node -e "const s=require('net').connect(${PORT},'127.0.0.1');const
|
|
38
|
+
done=(c)=>{try{s.destroy()}catch{};process.exit(c)};s.on('connect',()=>done(0));s.on('error',()=>done(1));setTimeout(()=>done(1),2000);"
|
|
39
|
+
interval: 30s
|
|
40
|
+
timeout: 10s
|
|
41
|
+
retries: 3
|
|
42
|
+
start_period: 15s
|
|
43
|
+
|
|
44
|
+
volumes:
|
|
45
|
+
limbo-data:
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
// ─── Colors ──────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const c = {
|
|
51
|
+
reset: '\x1b[0m',
|
|
52
|
+
bold: '\x1b[1m',
|
|
53
|
+
cyan: '\x1b[36m',
|
|
54
|
+
green: '\x1b[32m',
|
|
55
|
+
yellow: '\x1b[33m',
|
|
56
|
+
red: '\x1b[31m',
|
|
57
|
+
};
|
|
58
|
+
|
|
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); };
|
|
63
|
+
const header = (msg) => console.log(`\n${c.bold}${msg}${c.reset}`);
|
|
64
|
+
|
|
65
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function hasDocker() {
|
|
68
|
+
const result = spawnSync('docker', ['compose', 'version'], { stdio: 'pipe' });
|
|
69
|
+
return result.status === 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function run(cmd, opts = {}) {
|
|
73
|
+
return execSync(cmd, { stdio: 'inherit', cwd: LIMBO_DIR, ...opts });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function runQuiet(cmd) {
|
|
77
|
+
return execSync(cmd, { stdio: 'pipe', cwd: LIMBO_DIR }).toString().trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function prompt(rl, question) {
|
|
81
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function collectConfig() {
|
|
85
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
86
|
+
|
|
87
|
+
console.log('Limbo supports Anthropic (Claude) and OpenAI as model providers.');
|
|
88
|
+
console.log('Telegram integration is optional — press Enter to skip.\n');
|
|
89
|
+
|
|
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-...)';
|
|
94
|
+
|
|
95
|
+
let llmKey = '';
|
|
96
|
+
while (!llmKey) {
|
|
97
|
+
llmKey = (await prompt(rl, ` ${keyLabel}: `)).trim();
|
|
98
|
+
if (!llmKey) warn('This field is required.');
|
|
99
|
+
}
|
|
100
|
+
|
|
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.');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const tag = (await prompt(rl, ` Image tag [${DEFAULT_TAG}]: `)).trim() || DEFAULT_TAG;
|
|
113
|
+
|
|
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';
|
|
127
|
+
fs.writeFileSync(ENV_FILE, content, { mode: 0o600 });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function waitForHealthy(maxAttempts = 12) {
|
|
131
|
+
for (let i = 1; i <= maxAttempts; i++) {
|
|
132
|
+
try {
|
|
133
|
+
const raw = runQuiet('docker compose ps --format json');
|
|
134
|
+
if (raw.includes('"healthy"')) return true;
|
|
135
|
+
} 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);
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
async function cmdStart() {
|
|
146
|
+
header('=== Limbo ===');
|
|
147
|
+
|
|
148
|
+
if (!hasDocker()) {
|
|
149
|
+
die('Docker is not installed or `docker compose` is unavailable.\nInstall Docker Desktop: https://docs.docker.com/get-docker/');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fs.mkdirSync(LIMBO_DIR, { recursive: true });
|
|
153
|
+
fs.writeFileSync(COMPOSE_FILE, COMPOSE_CONTENT);
|
|
154
|
+
|
|
155
|
+
const alreadyHasEnv = fs.existsSync(ENV_FILE);
|
|
156
|
+
let cfg;
|
|
157
|
+
|
|
158
|
+
if (alreadyHasEnv) {
|
|
159
|
+
log(`Found existing config at ${ENV_FILE}`);
|
|
160
|
+
const reconfig = process.argv.includes('--reconfigure');
|
|
161
|
+
if (!reconfig) {
|
|
162
|
+
log('Starting with existing config. Use --reconfigure to change settings.');
|
|
163
|
+
cfg = null; // skip writing
|
|
164
|
+
} else {
|
|
165
|
+
header('Reconfiguration');
|
|
166
|
+
cfg = await collectConfig();
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
header('Configuration');
|
|
170
|
+
cfg = await collectConfig();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (cfg) {
|
|
174
|
+
writeEnv(cfg);
|
|
175
|
+
ok('.env written.');
|
|
176
|
+
}
|
|
177
|
+
|
|
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
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
header('Starting Limbo...');
|
|
197
|
+
run('docker compose up -d --remove-orphans');
|
|
198
|
+
|
|
199
|
+
header('Verifying health...');
|
|
200
|
+
const healthy = waitForHealthy();
|
|
201
|
+
if (!healthy) {
|
|
202
|
+
warn('Container did not report healthy within timeout.');
|
|
203
|
+
warn(`Check logs with: limbo logs`);
|
|
204
|
+
} else {
|
|
205
|
+
ok('Container is healthy.');
|
|
206
|
+
}
|
|
207
|
+
|
|
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
|
+
`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function cmdStop() {
|
|
222
|
+
if (!fs.existsSync(COMPOSE_FILE)) die('Limbo is not installed. Run: npx limbo start');
|
|
223
|
+
log('Stopping Limbo...');
|
|
224
|
+
run('docker compose down');
|
|
225
|
+
ok('Stopped.');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function cmdLogs() {
|
|
229
|
+
if (!fs.existsSync(COMPOSE_FILE)) die('Limbo is not installed. Run: npx limbo start');
|
|
230
|
+
run('docker compose logs -f');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function cmdUpdate() {
|
|
234
|
+
if (!fs.existsSync(COMPOSE_FILE)) die('Limbo is not installed. Run: npx limbo start');
|
|
235
|
+
log('Pulling latest image...');
|
|
236
|
+
run('docker compose pull -q');
|
|
237
|
+
log('Restarting...');
|
|
238
|
+
run('docker compose up -d --remove-orphans');
|
|
239
|
+
ok('Updated and restarted.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function cmdStatus() {
|
|
243
|
+
if (!fs.existsSync(COMPOSE_FILE)) {
|
|
244
|
+
log('Limbo is not installed.');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
run('docker compose ps');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function cmdHelp() {
|
|
251
|
+
console.log(`
|
|
252
|
+
${c.bold}limbo${c.reset} — personal AI memory agent
|
|
253
|
+
|
|
254
|
+
${c.bold}Usage:${c.reset}
|
|
255
|
+
npx limbo [command]
|
|
256
|
+
|
|
257
|
+
${c.bold}Commands:${c.reset}
|
|
258
|
+
start Install and start Limbo (default if no command given)
|
|
259
|
+
stop Stop the running container
|
|
260
|
+
logs Tail container logs
|
|
261
|
+
update Pull latest image and restart
|
|
262
|
+
status Show container status
|
|
263
|
+
help Show this help
|
|
264
|
+
|
|
265
|
+
${c.bold}Flags:${c.reset}
|
|
266
|
+
--reconfigure Reconfigure API keys and settings (use with start)
|
|
267
|
+
|
|
268
|
+
${c.bold}Data directory:${c.reset} ${LIMBO_DIR}
|
|
269
|
+
`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
const [,, cmd = 'start'] = process.argv;
|
|
275
|
+
|
|
276
|
+
(async () => {
|
|
277
|
+
switch (cmd) {
|
|
278
|
+
case 'start':
|
|
279
|
+
case 'install': await cmdStart(); break;
|
|
280
|
+
case 'stop': cmdStop(); break;
|
|
281
|
+
case 'logs': cmdLogs(); break;
|
|
282
|
+
case 'update': cmdUpdate(); break;
|
|
283
|
+
case 'status': cmdStatus(); break;
|
|
284
|
+
case 'help':
|
|
285
|
+
case '--help':
|
|
286
|
+
case '-h': cmdHelp(); break;
|
|
287
|
+
default:
|
|
288
|
+
warn(`Unknown command: ${cmd}`);
|
|
289
|
+
cmdHelp();
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
})().catch((err) => {
|
|
293
|
+
die(err.message || String(err));
|
|
294
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
|
|
8
|
+
import { vaultSearch } from "./tools/search.js";
|
|
9
|
+
import { vaultRead } from "./tools/read.js";
|
|
10
|
+
import { vaultWriteNote } from "./tools/write.js";
|
|
11
|
+
import { vaultUpdateMap } from "./tools/update-map.js";
|
|
12
|
+
|
|
13
|
+
const server = new Server(
|
|
14
|
+
{
|
|
15
|
+
name: "limbo-vault",
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
capabilities: {
|
|
20
|
+
tools: {},
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// ── Tool definitions ────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
28
|
+
tools: [
|
|
29
|
+
{
|
|
30
|
+
name: "vault_search",
|
|
31
|
+
description:
|
|
32
|
+
"Search notes in the vault by regex query. Returns matching notes with titles, snippets, and relevance scores.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
query: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Regex or keyword query to search across all vault notes",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ["query"],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "vault_read",
|
|
46
|
+
description:
|
|
47
|
+
"Read the full content of a vault note by ID. Returns raw markdown including YAML frontmatter.",
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {
|
|
51
|
+
noteId: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "The note ID (filename without .md extension)",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ["noteId"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "vault_write_note",
|
|
61
|
+
description:
|
|
62
|
+
"Create or overwrite a vault note with YAML frontmatter. Required fields: id, title, type, description, content. Optional: map.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
id: { type: "string", description: "Unique note identifier (alphanumeric, dashes, underscores)" },
|
|
67
|
+
title: { type: "string", description: "Human-readable note title" },
|
|
68
|
+
type: { type: "string", description: "Note type, e.g. claim, source, concept, question" },
|
|
69
|
+
description: { type: "string", description: "One-sentence description of the note's claim or content" },
|
|
70
|
+
content: { type: "string", description: "Full markdown body of the note" },
|
|
71
|
+
map: { type: "string", description: "Optional: name of the MOC this note belongs to" },
|
|
72
|
+
},
|
|
73
|
+
required: ["id", "title", "type", "description", "content"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "vault_update_map",
|
|
78
|
+
description:
|
|
79
|
+
"Append entries to a section in a Map of Content (MOC). Creates the map file and/or section if they don't exist.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
map: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Map filename without extension (alphanumeric, dashes, underscores)",
|
|
86
|
+
},
|
|
87
|
+
section: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Section heading text to append entries under",
|
|
90
|
+
},
|
|
91
|
+
entries: {
|
|
92
|
+
type: "array",
|
|
93
|
+
items: { type: "string" },
|
|
94
|
+
description: "Markdown link strings to append, e.g. [\"[[note-id|Note Title]]\"]",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
required: ["map", "section", "entries"],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
// ── Tool execution ──────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
106
|
+
const { name, arguments: args } = request.params;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
switch (name) {
|
|
110
|
+
case "vault_search": {
|
|
111
|
+
const results = await vaultSearch(args.query);
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case "vault_read": {
|
|
118
|
+
const content = await vaultRead(args.noteId);
|
|
119
|
+
if (content === null) {
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: "text", text: `Note not found: ${args.noteId}` }],
|
|
122
|
+
isError: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return { content: [{ type: "text", text: content }] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "vault_write_note": {
|
|
129
|
+
const result = await vaultWriteNote(args);
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: "text", text: `Note written: ${result.id}` }],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case "vault_update_map": {
|
|
136
|
+
const result = await vaultUpdateMap(args.map, args.section, args.entries);
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: `Map updated: ${result.map} — added ${result.added} entries to "${result.section}"`,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
default:
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
156
|
+
isError: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ── Start ───────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
const transport = new StdioServerTransport();
|
|
164
|
+
await server.connect(transport);
|