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/LICENSE +674 -0
- package/README.md +248 -0
- package/package.json +58 -0
- package/src/cli/commands/believe.ts +27 -0
- package/src/cli/commands/consolidate.ts +24 -0
- package/src/cli/commands/export-import.ts +91 -0
- package/src/cli/commands/goal.ts +71 -0
- package/src/cli/commands/health.ts +43 -0
- package/src/cli/commands/init.ts +90 -0
- package/src/cli/commands/observe.ts +40 -0
- package/src/cli/commands/query.ts +31 -0
- package/src/cli/commands/recall.ts +17 -0
- package/src/cli/commands/serve.ts +19 -0
- package/src/cli/commands/shared.ts +20 -0
- package/src/cli/commands/sync.ts +23 -0
- package/src/cli/commands/timeline.ts +37 -0
- package/src/cli/commands/wander.ts +34 -0
- package/src/cli/index.ts +41 -0
- package/src/cli/shared.ts +9 -0
- package/src/cognitive/consolidate.ts +349 -0
- package/src/cognitive/contradiction.ts +50 -0
- package/src/cognitive/health.ts +123 -0
- package/src/config.ts +114 -0
- package/src/db/client.ts +59 -0
- package/src/db/preflight.ts +152 -0
- package/src/db/schema.surql +109 -0
- package/src/db/schema.ts +24 -0
- package/src/env.d.ts +4 -0
- package/src/goal.ts +39 -0
- package/src/index.ts +13 -0
- package/src/logger.ts +60 -0
- package/src/mcp/server.ts +23 -0
- package/src/mcp/tools.ts +100 -0
- package/src/memory/episode.ts +66 -0
- package/src/memory/read.ts +223 -0
- package/src/memory/write.ts +134 -0
- package/src/sync.ts +120 -0
- package/src/types.ts +144 -0
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
|
+
})
|