suemo 0.0.1 → 0.0.3
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 +127 -27
- package/package.json +1 -1
- package/src/cli/commands/believe.ts +22 -12
- package/src/cli/commands/consolidate.ts +18 -11
- package/src/cli/commands/doctor.ts +123 -0
- package/src/cli/commands/export-import.ts +83 -47
- package/src/cli/commands/goal.ts +52 -27
- package/src/cli/commands/health.ts +53 -18
- package/src/cli/commands/init.ts +155 -75
- package/src/cli/commands/observe.ts +26 -13
- package/src/cli/commands/query.ts +23 -7
- package/src/cli/commands/recall.ts +12 -6
- package/src/cli/commands/serve.ts +25 -6
- package/src/cli/commands/sync.ts +44 -10
- package/src/cli/commands/timeline.ts +30 -18
- package/src/cli/commands/wander.ts +27 -16
- package/src/cli/index.ts +3 -4
- package/src/cli/shared.ts +34 -0
- package/src/cognitive/consolidate.ts +48 -19
- package/src/cognitive/contradiction.ts +19 -7
- package/src/cognitive/health.ts +61 -1
- package/src/config.template.ts +58 -0
- package/src/config.ts +124 -14
- package/src/db/preflight.ts +32 -6
- package/src/db/schema.surql +30 -9
- package/src/db/schema.ts +6 -3
- package/src/embedding/index.ts +52 -0
- package/src/embedding/openai-compatible.ts +43 -0
- package/src/goal.ts +3 -1
- package/src/index.ts +5 -1
- package/src/mcp/dispatch.ts +232 -0
- package/src/mcp/server.ts +150 -4
- package/src/mcp/stdio.ts +385 -0
- package/src/mcp/tools.ts +13 -90
- package/src/memory/episode.ts +92 -0
- package/src/memory/read.ts +76 -19
- package/src/memory/write.ts +253 -20
- package/src/sync.ts +310 -66
- package/src/types.ts +30 -5
- package/src/cli/commands/shared.ts +0 -20
package/README.md
CHANGED
|
@@ -58,23 +58,24 @@ bun install
|
|
|
58
58
|
**1. Create config**
|
|
59
59
|
|
|
60
60
|
```sh
|
|
61
|
-
bun run src/cli/index.ts init
|
|
62
|
-
# or, once linked: suemo init
|
|
61
|
+
bun run src/cli/index.ts init config
|
|
62
|
+
# or, once linked: suemo init config
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
This writes `~/.suemo/suemo.ts`. Edit it with your SurrealDB URL, credentials, and LLM endpoint.
|
|
66
66
|
|
|
67
67
|
**2. Apply schema**
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Apply schema after you set/edit namespace/database:
|
|
70
70
|
|
|
71
71
|
```sh
|
|
72
|
-
|
|
73
|
-
SURREAL_USER=root \
|
|
74
|
-
SURREAL_PASS=root \
|
|
75
|
-
suemo init
|
|
72
|
+
suemo init schema
|
|
76
73
|
```
|
|
77
74
|
|
|
75
|
+
Pass `--yes` to skip schema confirmation in non-interactive flows.
|
|
76
|
+
|
|
77
|
+
`suemo init` now shows guidance by default; run `init config` and `init schema` explicitly.
|
|
78
|
+
|
|
78
79
|
**3. Store a memory**
|
|
79
80
|
|
|
80
81
|
```sh
|
|
@@ -107,8 +108,10 @@ Global flags (inherited by all commands):
|
|
|
107
108
|
-d, --debug Verbose debug logging
|
|
108
109
|
|
|
109
110
|
Commands:
|
|
110
|
-
init
|
|
111
|
-
|
|
111
|
+
init Show init subcommands and usage guidance
|
|
112
|
+
init config Create/update ~/.suemo/suemo.ts
|
|
113
|
+
init schema Apply DB schema from current config (with confirm)
|
|
114
|
+
serve Start the MCP server (HTTP or stdio)
|
|
112
115
|
observe <content> Store an observation
|
|
113
116
|
believe <content> Store a belief (triggers contradiction detection)
|
|
114
117
|
query <input> Hybrid semantic search
|
|
@@ -119,9 +122,11 @@ Commands:
|
|
|
119
122
|
goal list List active goals
|
|
120
123
|
goal resolve <id> Mark a goal achieved
|
|
121
124
|
consolidate Run NREM + REM consolidation pipeline
|
|
125
|
+
doctor embed Diagnose fn::embed setup and print fix steps
|
|
122
126
|
health Memory health report
|
|
123
127
|
health vitals Last 10 consolidation runs + node counts
|
|
124
|
-
|
|
128
|
+
health stats Lightweight usage counters and timestamps
|
|
129
|
+
sync Sync memories with a remote SurrealDB instance
|
|
125
130
|
export Stream memories to JSONL on stdout
|
|
126
131
|
import <file> Import memories from a JSONL file
|
|
127
132
|
```
|
|
@@ -130,7 +135,16 @@ Commands:
|
|
|
130
135
|
|
|
131
136
|
## Config
|
|
132
137
|
|
|
133
|
-
Config lives at `~/.suemo/suemo.ts` (user-level) or `suemo.config.ts` in the project root. Project-local takes precedence.
|
|
138
|
+
Config lives at `~/.suemo/suemo.ts` (user-level) or `suemo.config.ts` in the project root. Project-local takes precedence.
|
|
139
|
+
|
|
140
|
+
Resolution order:
|
|
141
|
+
|
|
142
|
+
1. `--config <path>`
|
|
143
|
+
2. `SUEMO_CONFIG_PATH`
|
|
144
|
+
3. project-local (`suemo.config.ts` / `suemo.config.js`)
|
|
145
|
+
4. user-level (`~/.suemo/suemo.ts`)
|
|
146
|
+
|
|
147
|
+
Set `SUEMO_DEBUG=1` (or `true`, `yes`, `on`) to enable debug logging globally for CLI commands.
|
|
134
148
|
|
|
135
149
|
```ts
|
|
136
150
|
// ~/.suemo/suemo.ts
|
|
@@ -163,9 +177,56 @@ export default defineConfig({
|
|
|
163
177
|
weights: { vector: 0.5, bm25: 0.25, graph: 0.15, temporal: 0.1 },
|
|
164
178
|
},
|
|
165
179
|
mcp: { port: 4242, host: '127.0.0.1' },
|
|
180
|
+
sync: {
|
|
181
|
+
remotes: {
|
|
182
|
+
vps: {
|
|
183
|
+
url: process.env.SUEMO_SYNC_URL!,
|
|
184
|
+
namespace: process.env.SUEMO_SYNC_NS!,
|
|
185
|
+
database: process.env.SUEMO_SYNC_DB!,
|
|
186
|
+
auth: {
|
|
187
|
+
user: process.env.SUEMO_SYNC_USER!,
|
|
188
|
+
pass: process.env.SUEMO_SYNC_PASS!,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
defaultRemote: 'vps',
|
|
193
|
+
auto: {
|
|
194
|
+
enabled: false,
|
|
195
|
+
intervalSeconds: 300,
|
|
196
|
+
direction: 'push',
|
|
197
|
+
remote: 'vps',
|
|
198
|
+
onWrite: false,
|
|
199
|
+
minWriteIntervalSeconds: 30,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
166
202
|
})
|
|
167
203
|
```
|
|
168
204
|
|
|
205
|
+
### Sync config notes
|
|
206
|
+
|
|
207
|
+
- `sync.remotes` supports multiple named remote instances.
|
|
208
|
+
- `sync.defaultRemote` is used by `suemo sync` unless `--remote` is provided.
|
|
209
|
+
- Legacy `sync.remote` (single target) is still supported for migration compatibility.
|
|
210
|
+
- `sync.auto` powers background sync in `suemo serve` (HTTP and stdio):
|
|
211
|
+
- `enabled`: master switch
|
|
212
|
+
- `intervalSeconds`: timer cadence
|
|
213
|
+
- `direction`: `push | pull | both`
|
|
214
|
+
- `remote`: named target from `sync.remotes`
|
|
215
|
+
- `onWrite`: trigger sync after mutating MCP tools
|
|
216
|
+
- `minWriteIntervalSeconds`: throttle for write-triggered sync
|
|
217
|
+
|
|
218
|
+
Manual sync examples:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
suemo sync --direction both
|
|
222
|
+
suemo sync --remote vps --direction pull
|
|
223
|
+
suemo sync --remote vps --direction push --dry-run
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Minimal local↔VPS smoke test guide:
|
|
227
|
+
|
|
228
|
+
- `data/scenarios/sync-local-vps.md`
|
|
229
|
+
|
|
169
230
|
Multiple agents on the same machine use separate config files that extend a shared base:
|
|
170
231
|
|
|
171
232
|
```ts
|
|
@@ -181,26 +242,46 @@ export default {
|
|
|
181
242
|
suemo serve --config ~/.suemo/opencode.ts
|
|
182
243
|
```
|
|
183
244
|
|
|
245
|
+
For local stdio transport (no network port), run:
|
|
246
|
+
|
|
247
|
+
```sh
|
|
248
|
+
suemo serve --stdio
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
If stdio startup fails with an embedding preflight error, run:
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
suemo doctor embed
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
This prints your active target (`url`, `namespace`, `database`) and step-by-step commands to import a model and define `fn::embed()` for that exact database.
|
|
258
|
+
|
|
184
259
|
---
|
|
185
260
|
|
|
186
261
|
## MCP Tools
|
|
187
262
|
|
|
188
|
-
| Tool
|
|
189
|
-
|
|
|
190
|
-
| `observe`
|
|
191
|
-
| `believe`
|
|
192
|
-
| `invalidate`
|
|
193
|
-
| `query`
|
|
194
|
-
| `recall`
|
|
195
|
-
| `wander`
|
|
196
|
-
| `timeline`
|
|
197
|
-
| `episode_start`
|
|
198
|
-
| `episode_end`
|
|
199
|
-
| `goal_set`
|
|
200
|
-
| `goal_resolve`
|
|
201
|
-
| `goal_list`
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
263
|
+
| Tool | Description |
|
|
264
|
+
| --------------------- | ------------------------------------------------------------------- |
|
|
265
|
+
| `observe` | Store an observation, belief, question, or hypothesis |
|
|
266
|
+
| `believe` | Store a belief — auto-detects and invalidates contradicting beliefs |
|
|
267
|
+
| `invalidate` | Soft-delete a node by ID (sets `valid_until`) |
|
|
268
|
+
| `query` | Hybrid semantic search (vector + BM25 + graph) |
|
|
269
|
+
| `recall` | Fetch a node and its 1-hop neighbourhood; ticks FSRS |
|
|
270
|
+
| `wander` | Spreading-activation walk through the graph |
|
|
271
|
+
| `timeline` | Chronological memory slice with optional date range |
|
|
272
|
+
| `episode_start` | Begin a bounded session window |
|
|
273
|
+
| `episode_end` | Close a session, optionally with a summary |
|
|
274
|
+
| `goal_set` | Create a goal node |
|
|
275
|
+
| `goal_resolve` | Mark a goal achieved |
|
|
276
|
+
| `goal_list` | List active (or all) goals |
|
|
277
|
+
| `upsert_by_key` | Upsert a memory node by stable topic key |
|
|
278
|
+
| `capture_prompt` | Capture raw prompt and link derived observations |
|
|
279
|
+
| `session_context_get` | Fetch open episode summary/context by session ID |
|
|
280
|
+
| `session_context_set` | Update open episode summary/context by session ID |
|
|
281
|
+
| `consolidate` | Manually trigger NREM + REM consolidation |
|
|
282
|
+
| `health` | Graph health report |
|
|
283
|
+
| `vitals` | Last 10 consolidation runs + grouped node counts |
|
|
284
|
+
| `stats` | Lightweight usage stats and write/query counters |
|
|
204
285
|
|
|
205
286
|
Agents never supply temporal fields (`valid_from`, `valid_until`). These are system-managed.
|
|
206
287
|
|
|
@@ -231,6 +312,25 @@ The CLI and MCP server are both thin shells. All logic lives in the domain layer
|
|
|
231
312
|
|
|
232
313
|
---
|
|
233
314
|
|
|
315
|
+
## Giant end-to-end scenario (all tools / fields)
|
|
316
|
+
|
|
317
|
+
For full-system stress testing (CLI + MCP + consolidation + relation kinds), use:
|
|
318
|
+
|
|
319
|
+
- `data/scenarios/giant-scenario.md` — walkthrough + validation queries
|
|
320
|
+
- `data/scenarios/run-giant-cli.sh` — comprehensive CLI flow
|
|
321
|
+
- `data/scenarios/run-giant-mcp.mjs` — comprehensive stdio MCP flow
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
|
|
325
|
+
```sh
|
|
326
|
+
./data/scenarios/run-giant-cli.sh ~/.suemo/suemo.ts giant-cli-main
|
|
327
|
+
bun data/scenarios/run-giant-mcp.mjs --config ~/.suemo/suemo.ts --scope giant-mcp-main
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
These scenarios intentionally exercise all memory kinds and all relation kinds, and produce enough incident-like data to observe NREM/REM behavior.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
234
334
|
## Stack
|
|
235
335
|
|
|
236
336
|
- **Runtime** — [Bun](https://bun.sh)
|
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { loadConfig } from '../../config.ts'
|
|
2
2
|
import { connect, disconnect } from '../../db/client.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { getLogger } from '../../logger.ts'
|
|
4
4
|
import { believe } from '../../memory/write.ts'
|
|
5
|
-
import { app } from '../shared.ts'
|
|
5
|
+
import { app, initCliCommand } from '../shared.ts'
|
|
6
|
+
|
|
7
|
+
const log = getLogger(['suemo', 'cli', 'believe'])
|
|
6
8
|
|
|
7
9
|
export const believeCmd = app.sub('believe')
|
|
8
10
|
.meta({ description: 'Store a belief (triggers contradiction detection)' })
|
|
@@ -12,16 +14,24 @@ export const believeCmd = app.sub('believe')
|
|
|
12
14
|
confidence: { type: 'number', description: 'Confidence 0.0–1.0', default: 1.0 },
|
|
13
15
|
})
|
|
14
16
|
.run(async ({ args, flags }) => {
|
|
15
|
-
await
|
|
17
|
+
await initCliCommand('believe', { debug: flags.debug, config: flags.config })
|
|
16
18
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
17
19
|
const db = await connect(config.surreal)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
try {
|
|
21
|
+
log.debug('Running believe command', {
|
|
22
|
+
hasScope: Boolean(flags.scope),
|
|
23
|
+
confidence: flags.confidence,
|
|
24
|
+
contentLength: args.content.length,
|
|
25
|
+
})
|
|
26
|
+
const { node, contradicted } = await believe(db, {
|
|
27
|
+
content: args.content,
|
|
28
|
+
scope: flags.scope,
|
|
29
|
+
confidence: flags.confidence,
|
|
30
|
+
}, config)
|
|
31
|
+
const out: Record<string, unknown> = { id: node.id, valid_from: node.valid_from }
|
|
32
|
+
if (contradicted) out.contradicted = contradicted.id
|
|
33
|
+
console.log(JSON.stringify(out, null, 2))
|
|
34
|
+
} finally {
|
|
35
|
+
await disconnect()
|
|
36
|
+
}
|
|
27
37
|
})
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { consolidate } from '../../cognitive/consolidate.ts'
|
|
2
2
|
import { loadConfig } from '../../config.ts'
|
|
3
3
|
import { connect, disconnect } from '../../db/client.ts'
|
|
4
|
-
import {
|
|
5
|
-
import { app } from '../shared.ts'
|
|
4
|
+
import { getLogger } from '../../logger.ts'
|
|
5
|
+
import { app, initCliCommand } from '../shared.ts'
|
|
6
|
+
|
|
7
|
+
const log = getLogger(['suemo', 'cli', 'consolidate'])
|
|
6
8
|
|
|
7
9
|
export const consolidateCmd = app.sub('consolidate')
|
|
8
10
|
.meta({ description: 'Manually trigger memory consolidation (NREM + REM)' })
|
|
@@ -10,15 +12,20 @@ export const consolidateCmd = app.sub('consolidate')
|
|
|
10
12
|
'nrem-only': { type: 'boolean', description: 'Run only NREM (compression) phase', default: false },
|
|
11
13
|
})
|
|
12
14
|
.run(async ({ flags }) => {
|
|
13
|
-
await
|
|
15
|
+
await initCliCommand('consolidate', { debug: flags.debug, config: flags.config })
|
|
16
|
+
log.debug('Running consolidate command', { nremOnly: flags['nrem-only'] })
|
|
14
17
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
15
18
|
const db = await connect(config.surreal)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
try {
|
|
20
|
+
const run = await consolidate(db, {
|
|
21
|
+
nremOnly: flags['nrem-only'],
|
|
22
|
+
nremSimilarityThreshold: config.consolidation.nremSimilarityThreshold,
|
|
23
|
+
remRelationThreshold: config.consolidation.remRelationThreshold,
|
|
24
|
+
llm: config.consolidation.llm,
|
|
25
|
+
embedding: config.embedding,
|
|
26
|
+
})
|
|
27
|
+
console.log(JSON.stringify(run, null, 2))
|
|
28
|
+
} finally {
|
|
29
|
+
await disconnect()
|
|
30
|
+
}
|
|
24
31
|
})
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Surreal } from 'surrealdb'
|
|
2
|
+
import { loadConfig } from '../../config.ts'
|
|
3
|
+
import { connect, disconnect } from '../../db/client.ts'
|
|
4
|
+
import { checkCompatibility } from '../../db/preflight.ts'
|
|
5
|
+
import { getLogger } from '../../logger.ts'
|
|
6
|
+
import { app, initCliCommand } from '../shared.ts'
|
|
7
|
+
|
|
8
|
+
const log = getLogger(['suemo', 'cli', 'doctor'])
|
|
9
|
+
|
|
10
|
+
const doctor = app.sub('doctor')
|
|
11
|
+
.meta({ description: 'Diagnostics and setup guidance' })
|
|
12
|
+
|
|
13
|
+
function toCliEndpoint(url: string): string {
|
|
14
|
+
if (url.startsWith('ws://')) return `http://${url.slice('ws://'.length)}`
|
|
15
|
+
if (url.startsWith('wss://')) return `https://${url.slice('wss://'.length)}`
|
|
16
|
+
return url
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function printStatus(label: string, ok: boolean, detail?: string): void {
|
|
20
|
+
const prefix = ok ? '✓' : '✗'
|
|
21
|
+
if (detail) {
|
|
22
|
+
console.log(`${prefix} ${label}: ${detail}`)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
console.log(`${prefix} ${label}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function probeEmbed(db: Surreal): Promise<{ ok: boolean; error?: string }> {
|
|
29
|
+
try {
|
|
30
|
+
await db.query('RETURN fn::embed("suemo doctor embed probe")')
|
|
31
|
+
return { ok: true }
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return { ok: false, error: String(error) }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function detectModelNames(db: Surreal): Promise<string[]> {
|
|
38
|
+
try {
|
|
39
|
+
const result = await db.query<unknown[]>('INFO FOR DB')
|
|
40
|
+
const first = Array.isArray(result) ? result[0] : null
|
|
41
|
+
const row = Array.isArray(first) ? first[0] : first
|
|
42
|
+
if (!row || typeof row !== 'object') return []
|
|
43
|
+
const models = (row as { models?: unknown }).models
|
|
44
|
+
if (!models || typeof models !== 'object') return []
|
|
45
|
+
return Object.keys(models as Record<string, unknown>)
|
|
46
|
+
} catch {
|
|
47
|
+
return []
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const doctorEmbedCmd = doctor.sub('embed')
|
|
52
|
+
.meta({ description: 'Diagnose fn::embed() and show setup steps' })
|
|
53
|
+
.run(async ({ flags }) => {
|
|
54
|
+
await initCliCommand('doctor embed', { debug: flags.debug, config: flags.config })
|
|
55
|
+
|
|
56
|
+
const config = await loadConfig(process.cwd(), flags.config)
|
|
57
|
+
const endpoint = toCliEndpoint(config.surreal.url)
|
|
58
|
+
|
|
59
|
+
console.log('Embedding diagnostics target:')
|
|
60
|
+
console.log(` url: ${config.surreal.url}`)
|
|
61
|
+
console.log(` namespace: ${config.surreal.namespace}`)
|
|
62
|
+
console.log(` database: ${config.surreal.database}`)
|
|
63
|
+
console.log()
|
|
64
|
+
|
|
65
|
+
let db: Surreal | undefined
|
|
66
|
+
try {
|
|
67
|
+
db = await connect(config.surreal)
|
|
68
|
+
|
|
69
|
+
const compat = await checkCompatibility(db, {
|
|
70
|
+
requireEmbedding: false,
|
|
71
|
+
context: 'cli:doctor-embed',
|
|
72
|
+
})
|
|
73
|
+
const embedProbe = await probeEmbed(db)
|
|
74
|
+
const modelNames = await detectModelNames(db)
|
|
75
|
+
|
|
76
|
+
printStatus('SurrealDB version', compat.surrealVersion !== 'unknown', compat.surrealVersion)
|
|
77
|
+
printStatus('SurrealKV', compat.surrealkv)
|
|
78
|
+
printStatus('Retention >= 90d', compat.retention_ok)
|
|
79
|
+
printStatus('fn::embed()', embedProbe.ok, embedProbe.ok ? 'callable' : 'not callable')
|
|
80
|
+
printStatus(
|
|
81
|
+
'Imported models in DB',
|
|
82
|
+
modelNames.length > 0,
|
|
83
|
+
modelNames.length > 0 ? modelNames.join(', ') : 'none detected',
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if (embedProbe.ok) {
|
|
87
|
+
console.log('\n✅ fn::embed() is configured. You should be able to run:')
|
|
88
|
+
console.log(` suemo serve --stdio --config ${flags.config ?? '~/.suemo/suemo.ts'}`)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
log.warn('fn::embed probe failed', { error: embedProbe.error ?? 'unknown' })
|
|
93
|
+
console.log('\nHow to set up fn::embed() (step-by-step):')
|
|
94
|
+
console.log('\n1) Ensure SurrealDB CLI exposes ML commands:')
|
|
95
|
+
console.log(' surreal ml --help')
|
|
96
|
+
console.log('\n2) Import a .surml embedding model into this exact NS/DB:')
|
|
97
|
+
console.log(
|
|
98
|
+
` surreal ml import --conn ${endpoint} --user <USER> --pass <PASS> --ns ${config.surreal.namespace} --db ${config.surreal.database} path/to/your-model.surml`,
|
|
99
|
+
)
|
|
100
|
+
console.log('\n3) Define fn::embed wrapper to your imported model:')
|
|
101
|
+
console.log(
|
|
102
|
+
` surreal sql --conn ${endpoint} --user <USER> --pass <PASS> --ns ${config.surreal.namespace} --db ${config.surreal.database} --query "DEFINE FUNCTION OVERWRITE fn::embed(\\$text: string) { RETURN ml::your_model<1.0.0>(\\$text); };"`,
|
|
103
|
+
)
|
|
104
|
+
console.log('\n4) Verify function now works:')
|
|
105
|
+
console.log(
|
|
106
|
+
` surreal sql --conn ${endpoint} --user <USER> --pass <PASS> --ns ${config.surreal.namespace} --db ${config.surreal.database} --query 'RETURN fn::embed("suemo test");'`,
|
|
107
|
+
)
|
|
108
|
+
console.log('\n5) Start MCP stdio server:')
|
|
109
|
+
console.log(` suemo serve --stdio --config ${flags.config ?? '~/.suemo/suemo.ts'}`)
|
|
110
|
+
console.log('\nIf step 4 still fails, rerun: suemo doctor embed --config ~/.suemo/suemo.ts --debug')
|
|
111
|
+
} finally {
|
|
112
|
+
if (db) await disconnect()
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
export const doctorCmd = doctor
|
|
117
|
+
.command(doctorEmbedCmd)
|
|
118
|
+
.run(async ({ flags }) => {
|
|
119
|
+
await initCliCommand('doctor', { debug: flags.debug, config: flags.config })
|
|
120
|
+
console.log('Use one of:')
|
|
121
|
+
console.log(' suemo doctor embed')
|
|
122
|
+
console.log('\nRun `suemo doctor --help` for full details.')
|
|
123
|
+
})
|
|
@@ -2,9 +2,11 @@ import { createReadStream } from 'node:fs'
|
|
|
2
2
|
import { createInterface } from 'node:readline'
|
|
3
3
|
import { loadConfig } from '../../config.ts'
|
|
4
4
|
import { connect, disconnect } from '../../db/client.ts'
|
|
5
|
-
import {
|
|
5
|
+
import { getLogger } from '../../logger.ts'
|
|
6
6
|
import type { MemoryNode, Relation } from '../../types.ts'
|
|
7
|
-
import { app } from '../shared.ts'
|
|
7
|
+
import { app, initCliCommand } from '../shared.ts'
|
|
8
|
+
|
|
9
|
+
const log = getLogger(['suemo', 'cli', 'export-import'])
|
|
8
10
|
|
|
9
11
|
// ── export ─────────────────────────────────────────────────────────────────
|
|
10
12
|
export const exportCmd = app.sub('export')
|
|
@@ -14,31 +16,36 @@ export const exportCmd = app.sub('export')
|
|
|
14
16
|
all: { type: 'boolean', description: 'Include invalidated nodes' },
|
|
15
17
|
})
|
|
16
18
|
.run(async ({ flags }) => {
|
|
17
|
-
await
|
|
19
|
+
await initCliCommand('export', { debug: flags.debug, config: flags.config })
|
|
20
|
+
log.debug('Running export command', {
|
|
21
|
+
hasScope: Boolean(flags.scope),
|
|
22
|
+
includeInvalidated: Boolean(flags.all),
|
|
23
|
+
})
|
|
18
24
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
19
25
|
const db = await connect(config.surreal)
|
|
26
|
+
try {
|
|
27
|
+
const activeFilter = flags.all ? 'true' : '(valid_until = NONE OR valid_until > time::now())'
|
|
28
|
+
const scopeFilter = '($scope = NONE OR scope = $scope)'
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const [nodesResult, relationsResult] = await Promise.all([
|
|
25
|
-
db.query<[MemoryNode[]]>(
|
|
26
|
-
`
|
|
30
|
+
const [nodesResult, relationsResult] = await Promise.all([
|
|
31
|
+
db.query<[MemoryNode[]]>(
|
|
32
|
+
`
|
|
27
33
|
SELECT * FROM memory WHERE ${activeFilter} AND ${scopeFilter} ORDER BY created_at ASC
|
|
28
34
|
`,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
{ scope: flags.scope ?? null },
|
|
36
|
+
),
|
|
37
|
+
db.query<[Relation[]]>('SELECT * FROM relates_to ORDER BY created_at ASC'),
|
|
38
|
+
])
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
for (const node of nodesResult[0] ?? []) {
|
|
41
|
+
process.stdout.write(JSON.stringify({ _type: 'memory', ...node }) + '\n')
|
|
42
|
+
}
|
|
43
|
+
for (const rel of relationsResult[0] ?? []) {
|
|
44
|
+
process.stdout.write(JSON.stringify({ _type: 'relation', ...rel }) + '\n')
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
await disconnect()
|
|
39
48
|
}
|
|
40
|
-
|
|
41
|
-
await disconnect()
|
|
42
49
|
})
|
|
43
50
|
|
|
44
51
|
// ── import ──────────────────────────────────────────────────────────────────
|
|
@@ -46,7 +53,8 @@ export const importCmd = app.sub('import')
|
|
|
46
53
|
.meta({ description: 'Import memories from a JSONL file' })
|
|
47
54
|
.args([{ name: 'file', type: 'string', required: true }])
|
|
48
55
|
.run(async ({ args, flags }) => {
|
|
49
|
-
await
|
|
56
|
+
await initCliCommand('import', { debug: flags.debug, config: flags.config })
|
|
57
|
+
log.debug('Running import command', { file: args.file })
|
|
50
58
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
51
59
|
const db = await connect(config.surreal)
|
|
52
60
|
|
|
@@ -56,36 +64,64 @@ export const importCmd = app.sub('import')
|
|
|
56
64
|
let skipped = 0
|
|
57
65
|
let errors = 0
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
try {
|
|
68
|
+
for await (const line of rl) {
|
|
69
|
+
lineNum++
|
|
70
|
+
if (!line.trim()) continue
|
|
71
|
+
let row: Record<string, unknown>
|
|
72
|
+
try {
|
|
73
|
+
row = JSON.parse(line)
|
|
74
|
+
} catch {
|
|
75
|
+
console.error(`Line ${lineNum}: invalid JSON — stopping`)
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const type = row['_type']
|
|
80
|
+
delete row['_type']
|
|
69
81
|
|
|
70
|
-
|
|
71
|
-
|
|
82
|
+
try {
|
|
83
|
+
if (type === 'memory') {
|
|
84
|
+
await db.query('INSERT IGNORE INTO memory $row', { row })
|
|
85
|
+
imported++
|
|
86
|
+
} else if (type === 'relation') {
|
|
87
|
+
const inId = typeof row['in'] === 'string' ? row['in'] : undefined
|
|
88
|
+
const outId = typeof row['out'] === 'string' ? row['out'] : undefined
|
|
89
|
+
const kind = typeof row['kind'] === 'string' ? row['kind'] : undefined
|
|
90
|
+
const strength = typeof row['strength'] === 'number' ? row['strength'] : undefined
|
|
72
91
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
if (!inId || !outId || !kind || strength === undefined) {
|
|
93
|
+
skipped++
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await db.query(
|
|
98
|
+
`
|
|
99
|
+
LET $inRec = type::record($in);
|
|
100
|
+
LET $outRec = type::record($out);
|
|
101
|
+
RELATE $inRec->relates_to->$outRec CONTENT {
|
|
102
|
+
kind: $kind,
|
|
103
|
+
strength: $strength
|
|
104
|
+
};
|
|
105
|
+
`,
|
|
106
|
+
{
|
|
107
|
+
in: inId,
|
|
108
|
+
out: outId,
|
|
109
|
+
kind,
|
|
110
|
+
strength,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
imported++
|
|
114
|
+
} else {
|
|
115
|
+
skipped++
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error(`Line ${lineNum}: insert failed — ${String(e)}`)
|
|
119
|
+
errors++
|
|
82
120
|
}
|
|
83
|
-
} catch (e) {
|
|
84
|
-
console.error(`Line ${lineNum}: insert failed — ${String(e)}`)
|
|
85
|
-
errors++
|
|
86
121
|
}
|
|
122
|
+
} finally {
|
|
123
|
+
rl.close()
|
|
124
|
+
await disconnect()
|
|
87
125
|
}
|
|
88
|
-
|
|
89
|
-
await disconnect()
|
|
90
126
|
console.log(JSON.stringify({ imported, skipped, errors, lines: lineNum }, null, 2))
|
|
91
127
|
})
|