suemo 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # 🐘 suemo
2
+
3
+ > Persistent semantic memory for AI agents — backed by SurrealDB.
4
+
5
+ > [!CAUTION]
6
+ > **Bun-only.** This project will not run on Node.js and there are no plans to support it.
7
+ >
8
+ > suemo is experimental software built for the author's personal agent infrastructure. APIs may change without notice. Use at your own risk.
9
+
10
+ suemo gives AI agents a memory that survives across sessions, models, and runtimes. Write observations from a Telegram bot, query them from OpenCode, consolidate overnight — all agents share one source of truth in SurrealDB.
11
+
12
+ ---
13
+
14
+ ## Features
15
+
16
+ - **Hybrid retrieval** — vector (HNSW), BM25 full-text, and graph spreading activation, fused with reciprocal rank fusion server-side
17
+ - **Bi-temporal nodes** — every fact has `valid_from` / `valid_until`; nothing is hard-deleted
18
+ - **Contradiction detection** — calling `believe()` with a conflicting belief automatically invalidates the old one and links them with a `contradicts` edge
19
+ - **Two-phase consolidation** — NREM clusters and compresses redundant observations via LLM; REM integrates new summaries into the broader graph with auto-scored relations
20
+ - **SurrealKV time-travel** — `VERSION d'...'` queries let you inspect any node's state at any past datetime (requires `SURREAL_DATASTORE_RETENTION=90d`)
21
+ - **Namespace isolation** — one SurrealDB instance, multiple agents, zero collision (`namespace` = agent group, `database` = agent identity)
22
+ - **MCP + CLI** — both interfaces are thin shells over the same domain functions; no business logic duplication
23
+ - **Lightweight** — no bundled vector store, no embedded database process, no OpenAI SDK; embedding via `fn::embed()` runs server-side in SurrealDB
24
+
25
+ ---
26
+
27
+ ## Requirements
28
+
29
+ | Dependency | Version |
30
+ | ---------------------------------- | ------------------------- |
31
+ | [Bun](https://bun.sh) | ≄ 1.3 |
32
+ | [SurrealDB](https://surrealdb.com) | ≄ 3.0 (SurrealKV storage) |
33
+
34
+ SurrealDB must be started with SurrealKV and a retention window:
35
+
36
+ ```sh
37
+ SURREAL_DATASTORE_RETENTION=90d surreal start \
38
+ --bind 0.0.0.0:8000 \
39
+ surrealkv:///path/to/data
40
+ ```
41
+
42
+ An embedding model must be configured in SurrealDB for `fn::embed()` to work.
43
+
44
+ ---
45
+
46
+ ## Installation
47
+
48
+ ```sh
49
+ git clone https://github.com/UA-sensei/suemo
50
+ cd suemo
51
+ bun install
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Quick Start
57
+
58
+ **1. Create config**
59
+
60
+ ```sh
61
+ bun run src/cli/index.ts init
62
+ # or, once linked: suemo init
63
+ ```
64
+
65
+ This writes `~/.suemo/suemo.ts`. Edit it with your SurrealDB URL, credentials, and LLM endpoint.
66
+
67
+ **2. Apply schema**
68
+
69
+ `suemo init` applies the schema automatically if `SURREAL_*` env vars are set. Otherwise:
70
+
71
+ ```sh
72
+ SURREAL_URL=ws://localhost:8000 \
73
+ SURREAL_USER=root \
74
+ SURREAL_PASS=root \
75
+ suemo init
76
+ ```
77
+
78
+ **3. Store a memory**
79
+
80
+ ```sh
81
+ suemo observe "Bun is significantly faster than Node for CLI tools" \
82
+ --scope coding --tags bun,runtime
83
+ ```
84
+
85
+ **4. Query it**
86
+
87
+ ```sh
88
+ suemo query "which JS runtime is faster"
89
+ ```
90
+
91
+ **5. Start the MCP server**
92
+
93
+ ```sh
94
+ suemo serve
95
+ # Listening on http://127.0.0.1:4242
96
+ ```
97
+
98
+ ---
99
+
100
+ ## CLI Reference
101
+
102
+ ```
103
+ suemo <command> [options]
104
+
105
+ Global flags (inherited by all commands):
106
+ -c, --config <path> Path to config file
107
+ -d, --debug Verbose debug logging
108
+
109
+ Commands:
110
+ init Create config template and apply DB schema
111
+ serve Start the MCP server
112
+ observe <content> Store an observation
113
+ believe <content> Store a belief (triggers contradiction detection)
114
+ query <input> Hybrid semantic search
115
+ recall <nodeId> Fetch a node + its neighbours (ticks FSRS)
116
+ wander Spreading-activation walk through the memory graph
117
+ timeline Chronological view of memories
118
+ goal set <content> Create a goal node
119
+ goal list List active goals
120
+ goal resolve <id> Mark a goal achieved
121
+ consolidate Run NREM + REM consolidation pipeline
122
+ health Memory health report
123
+ health vitals Last 10 consolidation runs + node counts
124
+ sync Push memories to a remote SurrealDB instance
125
+ export Stream memories to JSONL on stdout
126
+ import <file> Import memories from a JSONL file
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Config
132
+
133
+ Config lives at `~/.suemo/suemo.ts` (user-level) or `suemo.config.ts` in the project root. Project-local takes precedence. Override with `--config <path>`.
134
+
135
+ ```ts
136
+ // ~/.suemo/suemo.ts
137
+ import { defineConfig } from 'suemo'
138
+
139
+ export default defineConfig({
140
+ surreal: {
141
+ url: process.env.SURREAL_URL!,
142
+ namespace: 'bearcu',
143
+ database: 'bearcu-telegram',
144
+ auth: {
145
+ user: process.env.SURREAL_USER!,
146
+ pass: process.env.SURREAL_PASS!,
147
+ },
148
+ },
149
+ embedding: {
150
+ provider: 'surreal', // fn::embed() — configured in SurrealDB
151
+ dimension: 1536,
152
+ },
153
+ consolidation: {
154
+ trigger: 'timer',
155
+ intervalMinutes: 30,
156
+ llm: {
157
+ url: process.env.LLM_URL!, // any OpenAI-compatible endpoint
158
+ model: process.env.LLM_MODEL!,
159
+ apiKey: process.env.LLM_API_KEY!,
160
+ },
161
+ },
162
+ retrieval: {
163
+ weights: { vector: 0.5, bm25: 0.25, graph: 0.15, temporal: 0.1 },
164
+ },
165
+ mcp: { port: 4242, host: '127.0.0.1' },
166
+ })
167
+ ```
168
+
169
+ Multiple agents on the same machine use separate config files that extend a shared base:
170
+
171
+ ```ts
172
+ // ~/.suemo/opencode.ts
173
+ import base from './suemo.ts'
174
+ export default {
175
+ ...base,
176
+ surreal: { ...base.surreal, database: 'bearcu-opencode' },
177
+ }
178
+ ```
179
+
180
+ ```sh
181
+ suemo serve --config ~/.suemo/opencode.ts
182
+ ```
183
+
184
+ ---
185
+
186
+ ## MCP Tools
187
+
188
+ | Tool | Description |
189
+ | --------------- | ------------------------------------------------------------------- |
190
+ | `observe` | Store an observation, belief, question, or hypothesis |
191
+ | `believe` | Store a belief — auto-detects and invalidates contradicting beliefs |
192
+ | `invalidate` | Soft-delete a node by ID (sets `valid_until`) |
193
+ | `query` | Hybrid semantic search (vector + BM25 + graph) |
194
+ | `recall` | Fetch a node and its 1-hop neighbourhood; ticks FSRS |
195
+ | `wander` | Spreading-activation walk through the graph |
196
+ | `timeline` | Chronological memory slice with optional date range |
197
+ | `episode_start` | Begin a bounded session window |
198
+ | `episode_end` | Close a session, optionally with a summary |
199
+ | `goal_set` | Create a goal node |
200
+ | `goal_resolve` | Mark a goal achieved |
201
+ | `goal_list` | List active (or all) goals |
202
+ | `consolidate` | Manually trigger NREM + REM consolidation |
203
+ | `health` | Graph health report |
204
+
205
+ Agents never supply temporal fields (`valid_from`, `valid_until`). These are system-managed.
206
+
207
+ ---
208
+
209
+ ## Architecture
210
+
211
+ ```
212
+ Agent / User
213
+ │
214
+ ā–¼
215
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
216
+ │ CLI │ │ MCP │
217
+ │ (crust) │ │ (Elysia) │
218
+ ā””ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”˜
219
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
220
+ ā–¼
221
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
222
+ │ Domain layer │ src/memory/, src/cognitive/, src/goal.ts
223
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
224
+ ā–¼
225
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
226
+ │ SurrealDB │ SurrealKV Ā· HNSW Ā· BM25 Ā· graph Ā· VERSION
227
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
228
+ ```
229
+
230
+ The CLI and MCP server are both thin shells. All logic lives in the domain layer — one implementation, two interfaces.
231
+
232
+ ---
233
+
234
+ ## Stack
235
+
236
+ - **Runtime** — [Bun](https://bun.sh)
237
+ - **Language** — TypeScript (strict, `exactOptionalPropertyTypes`)
238
+ - **Database** — [SurrealDB](https://surrealdb.com) v3.0+ (SurrealKV)
239
+ - **SDK** — [surrealdb.js](https://github.com/surrealdb/surrealdb.js) v2 + `@surrealdb/node`
240
+ - **CLI** — [crust](https://github.com/chenxin-yan/crust) (`@crustjs/core`)
241
+ - **HTTP / MCP** — [Elysia](https://elysiajs.com) v1.4
242
+ - **Validation** — [Zod](https://zod.dev) v4
243
+ - **Logging** — [LogTape](https://github.com/dahlia/logtape)
244
+ - **Linting** — [Biome](https://biomejs.dev)
245
+
246
+ ---
247
+
248
+ <p align="center"><sub><strong>Ā© 2025 MEDRIVIA ļ¼ Umar Alfarouk</strong></sub></p>
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "suemo",
3
+ "version": "0.0.1",
4
+ "description": "Persistent semantic memory for AI agents — backed by SurrealDB.",
5
+ "author": {
6
+ "name": "Umar Alfarouk",
7
+ "email": "medrivia@gmail.com",
8
+ "url": "https://alfarouk.id"
9
+ },
10
+ "license": "GPL-3.0-only",
11
+ "homepage": "https://github.com/mdrv/suemo",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/mdrv/suemo.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/mdrv/suemo/issues"
18
+ },
19
+ "keywords": ["surrealdb", "memory", "ai", "agent", "mcp", "vector-search", "semantic-memory"],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "type": "module",
24
+ "engines": {
25
+ "bun": ">=1.3"
26
+ },
27
+ "bin": {
28
+ "suemo": "./src/cli/index.ts"
29
+ },
30
+ "files": ["src", "LICENSE", "README.md"],
31
+ "exports": {
32
+ ".": {
33
+ "bun": "./src/index.ts",
34
+ "types": "./src/index.ts"
35
+ }
36
+ },
37
+ "scripts": {
38
+ "dev": "bun run src/cli/index.ts",
39
+ "start": "bun run src/cli/index.ts",
40
+ "check": "bun tsc --noEmit"
41
+ },
42
+ "dependencies": {
43
+ "@crustjs/core": "^0.0.15",
44
+ "@crustjs/plugins": "^0.0.19",
45
+ "@crustjs/prompts": "^0.0.9",
46
+ "@crustjs/style": "^0.0.5",
47
+ "@logtape/file": "^2.0.4",
48
+ "@logtape/logtape": "^2.0.4",
49
+ "@surrealdb/node": "^3.0.3",
50
+ "elysia": "^1.4.28",
51
+ "surrealdb": "^2.0.3",
52
+ "zod": "^4.3.6"
53
+ },
54
+ "devDependencies": {
55
+ "@types/bun": "^1.3.11",
56
+ "typescript": "^5.9.3"
57
+ }
58
+ }
@@ -0,0 +1,27 @@
1
+ import { loadConfig } from '../../config.ts'
2
+ import { connect, disconnect } from '../../db/client.ts'
3
+ import { initLogger } from '../../logger.ts'
4
+ import { believe } from '../../memory/write.ts'
5
+ import { app } from '../shared.ts'
6
+
7
+ export const believeCmd = app.sub('believe')
8
+ .meta({ description: 'Store a belief (triggers contradiction detection)' })
9
+ .args([{ name: 'content', type: 'string', required: true }])
10
+ .flags({
11
+ scope: { type: 'string', short: 's', description: 'Scope label' },
12
+ confidence: { type: 'number', description: 'Confidence 0.0–1.0', default: 1.0 },
13
+ })
14
+ .run(async ({ args, flags }) => {
15
+ await initLogger({ level: flags.debug ? 'debug' : 'info' })
16
+ const config = await loadConfig(process.cwd(), flags.config)
17
+ const db = await connect(config.surreal)
18
+ const { node, contradicted } = await believe(db, {
19
+ content: args.content,
20
+ scope: flags.scope,
21
+ confidence: flags.confidence,
22
+ })
23
+ await disconnect()
24
+ const out: Record<string, unknown> = { id: node.id, valid_from: node.valid_from }
25
+ if (contradicted) out.contradicted = contradicted.id
26
+ console.log(JSON.stringify(out, null, 2))
27
+ })
@@ -0,0 +1,24 @@
1
+ import { consolidate } from '../../cognitive/consolidate.ts'
2
+ import { loadConfig } from '../../config.ts'
3
+ import { connect, disconnect } from '../../db/client.ts'
4
+ import { initLogger } from '../../logger.ts'
5
+ import { app } from '../shared.ts'
6
+
7
+ export const consolidateCmd = app.sub('consolidate')
8
+ .meta({ description: 'Manually trigger memory consolidation (NREM + REM)' })
9
+ .flags({
10
+ 'nrem-only': { type: 'boolean', description: 'Run only NREM (compression) phase', default: false },
11
+ })
12
+ .run(async ({ flags }) => {
13
+ await initLogger({ level: flags.debug ? 'debug' : 'info' })
14
+ const config = await loadConfig(process.cwd(), flags.config)
15
+ const db = await connect(config.surreal)
16
+ const run = await consolidate(db, {
17
+ nremOnly: flags['nrem-only'],
18
+ nremSimilarityThreshold: config.consolidation.nremSimilarityThreshold,
19
+ remRelationThreshold: config.consolidation.remRelationThreshold,
20
+ llm: config.consolidation.llm,
21
+ })
22
+ await disconnect()
23
+ console.log(JSON.stringify(run, null, 2))
24
+ })
@@ -0,0 +1,91 @@
1
+ import { createReadStream } from 'node:fs'
2
+ import { createInterface } from 'node:readline'
3
+ import { loadConfig } from '../../config.ts'
4
+ import { connect, disconnect } from '../../db/client.ts'
5
+ import { initLogger } from '../../logger.ts'
6
+ import type { MemoryNode, Relation } from '../../types.ts'
7
+ import { app } from '../shared.ts'
8
+
9
+ // ── export ─────────────────────────────────────────────────────────────────
10
+ export const exportCmd = app.sub('export')
11
+ .meta({ description: 'Export memories to JSONL on stdout' })
12
+ .flags({
13
+ scope: { type: 'string', short: 's', description: 'Filter by scope' },
14
+ all: { type: 'boolean', description: 'Include invalidated nodes' },
15
+ })
16
+ .run(async ({ flags }) => {
17
+ await initLogger({ level: flags.debug ? 'debug' : 'info' })
18
+ const config = await loadConfig(process.cwd(), flags.config)
19
+ const db = await connect(config.surreal)
20
+
21
+ const activeFilter = flags.all ? 'true' : '(valid_until = NONE OR valid_until > time::now())'
22
+ const scopeFilter = '($scope = NONE OR scope = $scope)'
23
+
24
+ const [nodesResult, relationsResult] = await Promise.all([
25
+ db.query<[MemoryNode[]]>(
26
+ `
27
+ SELECT * FROM memory WHERE ${activeFilter} AND ${scopeFilter} ORDER BY created_at ASC
28
+ `,
29
+ { scope: flags.scope ?? null },
30
+ ),
31
+ db.query<[Relation[]]>('SELECT * FROM relates_to ORDER BY created_at ASC'),
32
+ ])
33
+
34
+ for (const node of nodesResult[0] ?? []) {
35
+ process.stdout.write(JSON.stringify({ _type: 'memory', ...node }) + '\n')
36
+ }
37
+ for (const rel of relationsResult[0] ?? []) {
38
+ process.stdout.write(JSON.stringify({ _type: 'relation', ...rel }) + '\n')
39
+ }
40
+
41
+ await disconnect()
42
+ })
43
+
44
+ // ── import ──────────────────────────────────────────────────────────────────
45
+ export const importCmd = app.sub('import')
46
+ .meta({ description: 'Import memories from a JSONL file' })
47
+ .args([{ name: 'file', type: 'string', required: true }])
48
+ .run(async ({ args, flags }) => {
49
+ await initLogger({ level: flags.debug ? 'debug' : 'info' })
50
+ const config = await loadConfig(process.cwd(), flags.config)
51
+ const db = await connect(config.surreal)
52
+
53
+ const rl = createInterface({ input: createReadStream(args.file) })
54
+ let lineNum = 0
55
+ let imported = 0
56
+ let skipped = 0
57
+ let errors = 0
58
+
59
+ for await (const line of rl) {
60
+ lineNum++
61
+ if (!line.trim()) continue
62
+ let row: Record<string, unknown>
63
+ try {
64
+ row = JSON.parse(line)
65
+ } catch {
66
+ console.error(`Line ${lineNum}: invalid JSON — stopping`)
67
+ break
68
+ }
69
+
70
+ const type = row['_type']
71
+ delete row['_type']
72
+
73
+ try {
74
+ if (type === 'memory') {
75
+ await db.query('INSERT IGNORE INTO memory $row', { row })
76
+ imported++
77
+ } else if (type === 'relation') {
78
+ await db.query('INSERT IGNORE INTO relates_to $row', { row })
79
+ imported++
80
+ } else {
81
+ skipped++
82
+ }
83
+ } catch (e) {
84
+ console.error(`Line ${lineNum}: insert failed — ${String(e)}`)
85
+ errors++
86
+ }
87
+ }
88
+
89
+ await disconnect()
90
+ console.log(JSON.stringify({ imported, skipped, errors, lines: lineNum }, null, 2))
91
+ })
@@ -0,0 +1,71 @@
1
+ import { loadConfig } from '../../config.ts'
2
+ import { connect, disconnect } from '../../db/client.ts'
3
+ import { goalList, goalResolve, goalSet } from '../../goal.ts'
4
+ import { initLogger } from '../../logger.ts'
5
+ import { app } from '../shared.ts'
6
+
7
+ const goal = app.sub('goal')
8
+ .meta({ description: 'Manage goal nodes' })
9
+
10
+ // suemo goal set "finish suemo v1"
11
+ const setCmd = goal.sub('set')
12
+ .meta({ description: 'Create a new goal' })
13
+ .args([{ name: 'content', type: 'string', required: true }])
14
+ .flags({
15
+ scope: { type: 'string', short: 's', description: 'Scope label' },
16
+ tags: { type: 'string', short: 't', description: 'Comma-separated tags' },
17
+ })
18
+ .run(async ({ args, flags }) => {
19
+ await initLogger({ level: flags.debug ? 'debug' : 'info' })
20
+ const config = await loadConfig(process.cwd(), flags.config)
21
+ const db = await connect(config.surreal)
22
+ const node = await goalSet(db, args.content, {
23
+ ...(flags.scope ? { scope: flags.scope } : {}),
24
+ tags: flags.tags ? flags.tags.split(',').map((t) => t.trim()) : [],
25
+ })
26
+ await disconnect()
27
+ console.log(JSON.stringify({ id: node.id, content: node.content }, null, 2))
28
+ })
29
+
30
+ // suemo goal list
31
+ const listCmd = goal.sub('list')
32
+ .meta({ description: 'List active goals' })
33
+ .flags({
34
+ scope: { type: 'string', short: 's', description: 'Filter by scope' },
35
+ resolved: { type: 'boolean', description: 'Include resolved goals', default: false },
36
+ })
37
+ .run(async ({ flags }) => {
38
+ await initLogger({ level: flags.debug ? 'debug' : 'info' })
39
+ const config = await loadConfig(process.cwd(), flags.config)
40
+ const db = await connect(config.surreal)
41
+ const goals = await goalList(db, {
42
+ ...(flags.scope ? { scope: flags.scope } : {}),
43
+ includeResolved: flags.resolved,
44
+ })
45
+ await disconnect()
46
+ for (const g of goals) {
47
+ const status = g.valid_until ? `resolved ${g.valid_until}` : 'active'
48
+ console.log(`[${status}] ${g.id}`)
49
+ console.log(` ${g.content}`)
50
+ console.log()
51
+ }
52
+ })
53
+
54
+ // suemo goal resolve <id>
55
+ const resolveCmd = goal.sub('resolve')
56
+ .meta({ description: 'Mark a goal as resolved' })
57
+ .args([{ name: 'goalId', type: 'string', required: true }])
58
+ .run(async ({ args, flags }) => {
59
+ await initLogger({ level: flags.debug ? 'debug' : 'info' })
60
+ const config = await loadConfig(process.cwd(), flags.config)
61
+ const db = await connect(config.surreal)
62
+ await goalResolve(db, args.goalId)
63
+ await disconnect()
64
+ console.log(`āœ“ Goal ${args.goalId} resolved`)
65
+ })
66
+
67
+ // Export the container with its subcommands registered
68
+ export const goalCmd = goal
69
+ .command(setCmd)
70
+ .command(listCmd)
71
+ .command(resolveCmd)
@@ -0,0 +1,43 @@
1
+ import { healthReport, vitals } from '../../cognitive/health.ts'
2
+ import { loadConfig } from '../../config.ts'
3
+ import { connect, disconnect } from '../../db/client.ts'
4
+ import { initLogger } from '../../logger.ts'
5
+ import { app } from '../shared.ts'
6
+
7
+ const health = app.sub('health')
8
+ .meta({ description: 'Memory health report and vitals' })
9
+
10
+ const reportCmd = health.sub('report')
11
+ .meta({ description: 'Full health report (default)' })
12
+ .run(async ({ flags }) => {
13
+ await initLogger({ level: flags.debug ? 'debug' : 'info' })
14
+ const config = await loadConfig(process.cwd(), flags.config)
15
+ const db = await connect(config.surreal)
16
+ const report = await healthReport(db)
17
+ await disconnect()
18
+ console.log(JSON.stringify(report, null, 2))
19
+ })
20
+
21
+ const vitalsCmd = health.sub('vitals')
22
+ .meta({ description: 'Last 10 consolidation runs + node counts' })
23
+ .run(async ({ flags }) => {
24
+ await initLogger({ level: flags.debug ? 'debug' : 'info' })
25
+ const config = await loadConfig(process.cwd(), flags.config)
26
+ const db = await connect(config.surreal)
27
+ const v = await vitals(db)
28
+ await disconnect()
29
+ console.log(JSON.stringify(v, null, 2))
30
+ })
31
+
32
+ export const healthCmd = health
33
+ .command(reportCmd)
34
+ .command(vitalsCmd)
35
+ // Default: run the report when just `suemo health` is called
36
+ .run(async ({ flags }) => {
37
+ await initLogger({ level: flags.debug ? 'debug' : 'info' })
38
+ const config = await loadConfig(process.cwd(), flags.config)
39
+ const db = await connect(config.surreal)
40
+ const report = await healthReport(db)
41
+ await disconnect()
42
+ console.log(JSON.stringify(report, null, 2))
43
+ })
@@ -0,0 +1,90 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { loadConfig } from '../../config.ts'
4
+ import { connect, disconnect } from '../../db/client.ts'
5
+ import { checkCompatibility } from '../../db/preflight.ts'
6
+ import { runSchema } from '../../db/schema.ts'
7
+ import { initLogger } from '../../logger.ts'
8
+ import { app } from '../shared.ts'
9
+
10
+ export const initCmd = app.sub('init')
11
+ .meta({ description: 'Initialize suemo: create config template and apply DB schema' })
12
+ .flags({
13
+ force: { type: 'boolean', description: 'Overwrite existing config without prompting' },
14
+ })
15
+ .run(async ({ flags }) => {
16
+ await initLogger({ level: flags.debug ? 'debug' : 'info' })
17
+
18
+ const homeConfig = join(process.env.HOME ?? '~', '.suemo', 'suemo.ts')
19
+ const configExists = existsSync(homeConfig)
20
+
21
+ if (configExists && !flags.force) {
22
+ console.log(`Config already exists at ${homeConfig}`)
23
+ console.log('Pass --force to overwrite.')
24
+ return
25
+ }
26
+
27
+ const template = `import { defineConfig } from "suemo";
28
+
29
+ export default defineConfig({
30
+ surreal: {
31
+ url: process.env.SURREAL_URL ?? "ws://localhost:8000",
32
+ namespace: process.env.SURREAL_NS ?? "myagents",
33
+ database: process.env.SURREAL_DB ?? "default",
34
+ auth: {
35
+ user: process.env.SURREAL_USER!,
36
+ pass: process.env.SURREAL_PASS!,
37
+ },
38
+ },
39
+ embedding: {
40
+ provider: "surreal",
41
+ dimension: 1536,
42
+ },
43
+ consolidation: {
44
+ trigger: "timer",
45
+ intervalMinutes: 30,
46
+ reactiveThreshold: 50,
47
+ nremSimilarityThreshold: 0.85,
48
+ remRelationThreshold: 0.4,
49
+ llm: {
50
+ url: process.env.LLM_URL!,
51
+ model: process.env.LLM_MODEL ?? "gpt-4o",
52
+ apiKey: process.env.LLM_API_KEY!,
53
+ },
54
+ },
55
+ retrieval: {
56
+ weights: { vector: 0.5, bm25: 0.25, graph: 0.15, temporal: 0.1 },
57
+ },
58
+ mcp: {
59
+ port: Number(process.env.SUEMO_PORT) || 4242,
60
+ host: "127.0.0.1",
61
+ },
62
+ });
63
+ `
64
+
65
+ mkdirSync(join(process.env.HOME ?? '~', '.suemo'), { recursive: true })
66
+ writeFileSync(homeConfig, template, 'utf-8')
67
+ console.log(`āœ“ Config written to ${homeConfig}`)
68
+
69
+ // Apply schema if SURREAL_* env vars are present
70
+ if (process.env.SURREAL_URL && process.env.SURREAL_USER) {
71
+ try {
72
+ const config = await loadConfig(process.cwd(), flags.config)
73
+ const db = await connect(config.surreal)
74
+ const compat = await checkCompatibility(db)
75
+ if (!compat.ok) {
76
+ console.error('\nāœ— Compatibility check failed:')
77
+ for (const e of compat.errors) console.error(` • ${e}`)
78
+ await disconnect()
79
+ return
80
+ }
81
+ await runSchema(db)
82
+ await disconnect()
83
+ console.log('āœ“ Schema applied to SurrealDB')
84
+ } catch (e) {
85
+ console.warn(` (Schema not applied — configure SURREAL_* env vars first: ${String(e)})`)
86
+ }
87
+ } else {
88
+ console.log(' Set SURREAL_URL, SURREAL_USER, SURREAL_PASS, then run `suemo init` again to apply schema.')
89
+ }
90
+ })