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.
Files changed (40) hide show
  1. package/README.md +127 -27
  2. package/package.json +1 -1
  3. package/src/cli/commands/believe.ts +22 -12
  4. package/src/cli/commands/consolidate.ts +18 -11
  5. package/src/cli/commands/doctor.ts +123 -0
  6. package/src/cli/commands/export-import.ts +83 -47
  7. package/src/cli/commands/goal.ts +52 -27
  8. package/src/cli/commands/health.ts +53 -18
  9. package/src/cli/commands/init.ts +155 -75
  10. package/src/cli/commands/observe.ts +26 -13
  11. package/src/cli/commands/query.ts +23 -7
  12. package/src/cli/commands/recall.ts +12 -6
  13. package/src/cli/commands/serve.ts +25 -6
  14. package/src/cli/commands/sync.ts +44 -10
  15. package/src/cli/commands/timeline.ts +30 -18
  16. package/src/cli/commands/wander.ts +27 -16
  17. package/src/cli/index.ts +3 -4
  18. package/src/cli/shared.ts +34 -0
  19. package/src/cognitive/consolidate.ts +48 -19
  20. package/src/cognitive/contradiction.ts +19 -7
  21. package/src/cognitive/health.ts +61 -1
  22. package/src/config.template.ts +58 -0
  23. package/src/config.ts +124 -14
  24. package/src/db/preflight.ts +32 -6
  25. package/src/db/schema.surql +30 -9
  26. package/src/db/schema.ts +6 -3
  27. package/src/embedding/index.ts +52 -0
  28. package/src/embedding/openai-compatible.ts +43 -0
  29. package/src/goal.ts +3 -1
  30. package/src/index.ts +5 -1
  31. package/src/mcp/dispatch.ts +232 -0
  32. package/src/mcp/server.ts +150 -4
  33. package/src/mcp/stdio.ts +385 -0
  34. package/src/mcp/tools.ts +13 -90
  35. package/src/memory/episode.ts +92 -0
  36. package/src/memory/read.ts +76 -19
  37. package/src/memory/write.ts +253 -20
  38. package/src/sync.ts +310 -66
  39. package/src/types.ts +30 -5
  40. 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
- `suemo init` applies the schema automatically if `SURREAL_*` env vars are set. Otherwise:
69
+ Apply schema after you set/edit namespace/database:
70
70
 
71
71
  ```sh
72
- SURREAL_URL=ws://localhost:8000 \
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 Create config template and apply DB schema
111
- serve Start the MCP server
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
- sync Push memories to a remote SurrealDB instance
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. Override with `--config <path>`.
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 | 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 |
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,6 +1,6 @@
1
1
  {
2
2
  "name": "suemo",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Persistent semantic memory for AI agents — backed by SurrealDB.",
5
5
  "author": {
6
6
  "name": "Umar Alfarouk",
@@ -1,8 +1,10 @@
1
1
  import { loadConfig } from '../../config.ts'
2
2
  import { connect, disconnect } from '../../db/client.ts'
3
- import { initLogger } from '../../logger.ts'
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 initLogger({ level: flags.debug ? 'debug' : 'info' })
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
- 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))
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 { initLogger } from '../../logger.ts'
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 initLogger({ level: flags.debug ? 'debug' : 'info' })
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
- 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))
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 { initLogger } from '../../logger.ts'
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 initLogger({ level: flags.debug ? 'debug' : 'info' })
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
- 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
- `
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
- { scope: flags.scope ?? null },
30
- ),
31
- db.query<[Relation[]]>('SELECT * FROM relates_to ORDER BY created_at ASC'),
32
- ])
35
+ { scope: flags.scope ?? null },
36
+ ),
37
+ db.query<[Relation[]]>('SELECT * FROM relates_to ORDER BY created_at ASC'),
38
+ ])
33
39
 
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')
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 initLogger({ level: flags.debug ? 'debug' : 'info' })
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
- 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
- }
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
- const type = row['_type']
71
- delete row['_type']
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
- 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++
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
  })