suemo 0.0.2 → 0.0.4

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 CHANGED
@@ -96,6 +96,14 @@ suemo serve
96
96
  # Listening on http://127.0.0.1:4242
97
97
  ```
98
98
 
99
+ For development with automatic restart on file changes:
100
+
101
+ ```sh
102
+ suemo serve --dev
103
+ ```
104
+
105
+ `--dev` re-runs `suemo serve` under Bun watch mode and restarts MCP when source files change.
106
+
99
107
  ---
100
108
 
101
109
  ## CLI Reference
@@ -125,7 +133,8 @@ Commands:
125
133
  doctor embed Diagnose fn::embed setup and print fix steps
126
134
  health Memory health report
127
135
  health vitals Last 10 consolidation runs + node counts
128
- sync Push memories to a remote SurrealDB instance
136
+ health stats Lightweight usage counters and timestamps
137
+ sync Sync memories with a remote SurrealDB instance
129
138
  export Stream memories to JSONL on stdout
130
139
  import <file> Import memories from a JSONL file
131
140
  ```
@@ -160,8 +169,8 @@ export default defineConfig({
160
169
  },
161
170
  },
162
171
  embedding: {
163
- provider: 'surreal', // fn::embed() — configured in SurrealDB
164
- dimension: 1536,
172
+ provider: 'surrealml', // fn::embed() — configured in SurrealDB
173
+ dimension: 384,
165
174
  },
166
175
  consolidation: {
167
176
  trigger: 'timer',
@@ -176,9 +185,56 @@ export default defineConfig({
176
185
  weights: { vector: 0.5, bm25: 0.25, graph: 0.15, temporal: 0.1 },
177
186
  },
178
187
  mcp: { port: 4242, host: '127.0.0.1' },
188
+ sync: {
189
+ remotes: {
190
+ vps: {
191
+ url: process.env.SUEMO_SYNC_URL!,
192
+ namespace: process.env.SUEMO_SYNC_NS!,
193
+ database: process.env.SUEMO_SYNC_DB!,
194
+ auth: {
195
+ user: process.env.SUEMO_SYNC_USER!,
196
+ pass: process.env.SUEMO_SYNC_PASS!,
197
+ },
198
+ },
199
+ },
200
+ defaultRemote: 'vps',
201
+ auto: {
202
+ enabled: false,
203
+ intervalSeconds: 300,
204
+ direction: 'push',
205
+ remote: 'vps',
206
+ onWrite: false,
207
+ minWriteIntervalSeconds: 30,
208
+ },
209
+ },
179
210
  })
180
211
  ```
181
212
 
213
+ ### Sync config notes
214
+
215
+ - `sync.remotes` supports multiple named remote instances.
216
+ - `sync.defaultRemote` is used by `suemo sync` unless `--remote` is provided.
217
+ - Legacy `sync.remote` (single target) is still supported for migration compatibility.
218
+ - `sync.auto` powers background sync in `suemo serve` (HTTP and stdio):
219
+ - `enabled`: master switch
220
+ - `intervalSeconds`: timer cadence
221
+ - `direction`: `push | pull | both`
222
+ - `remote`: named target from `sync.remotes`
223
+ - `onWrite`: trigger sync after mutating MCP tools
224
+ - `minWriteIntervalSeconds`: throttle for write-triggered sync
225
+
226
+ Manual sync examples:
227
+
228
+ ```sh
229
+ suemo sync --direction both
230
+ suemo sync --remote vps --direction pull
231
+ suemo sync --remote vps --direction push --dry-run
232
+ ```
233
+
234
+ Minimal local↔VPS smoke test guide:
235
+
236
+ - `data/scenarios/sync-local-vps.md`
237
+
182
238
  Multiple agents on the same machine use separate config files that extend a shared base:
183
239
 
184
240
  ```ts
@@ -212,22 +268,28 @@ This prints your active target (`url`, `namespace`, `database`) and step-by-step
212
268
 
213
269
  ## MCP Tools
214
270
 
215
- | Tool | Description |
216
- | --------------- | ------------------------------------------------------------------- |
217
- | `observe` | Store an observation, belief, question, or hypothesis |
218
- | `believe` | Store a belief — auto-detects and invalidates contradicting beliefs |
219
- | `invalidate` | Soft-delete a node by ID (sets `valid_until`) |
220
- | `query` | Hybrid semantic search (vector + BM25 + graph) |
221
- | `recall` | Fetch a node and its 1-hop neighbourhood; ticks FSRS |
222
- | `wander` | Spreading-activation walk through the graph |
223
- | `timeline` | Chronological memory slice with optional date range |
224
- | `episode_start` | Begin a bounded session window |
225
- | `episode_end` | Close a session, optionally with a summary |
226
- | `goal_set` | Create a goal node |
227
- | `goal_resolve` | Mark a goal achieved |
228
- | `goal_list` | List active (or all) goals |
229
- | `consolidate` | Manually trigger NREM + REM consolidation |
230
- | `health` | Graph health report |
271
+ | Tool | Description |
272
+ | --------------------- | ------------------------------------------------------------------- |
273
+ | `observe` | Store an observation, belief, question, or hypothesis |
274
+ | `believe` | Store a belief — auto-detects and invalidates contradicting beliefs |
275
+ | `invalidate` | Soft-delete a node by ID (sets `valid_until`) |
276
+ | `query` | Hybrid semantic search (vector + BM25 + graph) |
277
+ | `recall` | Fetch a node and its 1-hop neighbourhood; ticks FSRS |
278
+ | `wander` | Spreading-activation walk through the graph |
279
+ | `timeline` | Chronological memory slice with optional date range |
280
+ | `episode_start` | Begin a bounded session window |
281
+ | `episode_end` | Close a session, optionally with a summary |
282
+ | `goal_set` | Create a goal node |
283
+ | `goal_resolve` | Mark a goal achieved |
284
+ | `goal_list` | List active (or all) goals |
285
+ | `upsert_by_key` | Upsert a memory node by stable topic key |
286
+ | `capture_prompt` | Capture raw prompt and link derived observations |
287
+ | `session_context_get` | Fetch open episode summary/context by session ID |
288
+ | `session_context_set` | Update open episode summary/context by session ID |
289
+ | `consolidate` | Manually trigger NREM + REM consolidation |
290
+ | `health` | Graph health report |
291
+ | `vitals` | Last 10 consolidation runs + grouped node counts |
292
+ | `stats` | Lightweight usage stats and write/query counters |
231
293
 
232
294
  Agents never supply temporal fields (`valid_from`, `valid_until`). These are system-managed.
233
295
 
@@ -258,6 +320,25 @@ The CLI and MCP server are both thin shells. All logic lives in the domain layer
258
320
 
259
321
  ---
260
322
 
323
+ ## Giant end-to-end scenario (all tools / fields)
324
+
325
+ For full-system stress testing (CLI + MCP + consolidation + relation kinds), use:
326
+
327
+ - `data/scenarios/giant-scenario.md` — walkthrough + validation queries
328
+ - `data/scenarios/run-giant-cli.sh` — comprehensive CLI flow
329
+ - `data/scenarios/run-giant-mcp.mjs` — comprehensive stdio MCP flow
330
+
331
+ Example:
332
+
333
+ ```sh
334
+ ./data/scenarios/run-giant-cli.sh ~/.suemo/suemo.ts giant-cli-main
335
+ bun data/scenarios/run-giant-mcp.mjs --config ~/.suemo/suemo.ts --scope giant-mcp-main
336
+ ```
337
+
338
+ These scenarios intentionally exercise all memory kinds and all relation kinds, and produce enough incident-like data to observe NREM/REM behavior.
339
+
340
+ ---
341
+
261
342
  ## Stack
262
343
 
263
344
  - **Runtime** — [Bun](https://bun.sh)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suemo",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Persistent semantic memory for AI agents — backed by SurrealDB.",
5
5
  "author": {
6
6
  "name": "Umar Alfarouk",
@@ -84,7 +84,32 @@ export const importCmd = app.sub('import')
84
84
  await db.query('INSERT IGNORE INTO memory $row', { row })
85
85
  imported++
86
86
  } else if (type === 'relation') {
87
- await db.query('INSERT IGNORE INTO relates_to $row', { row })
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
91
+
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
+ )
88
113
  imported++
89
114
  } else {
90
115
  skipped++
@@ -1,5 +1,5 @@
1
1
  import type { Surreal } from 'surrealdb'
2
- import { healthReport, vitals } from '../../cognitive/health.ts'
2
+ import { healthReport, suemoStats, vitals } from '../../cognitive/health.ts'
3
3
  import { loadConfig } from '../../config.ts'
4
4
  import { connect, disconnect } from '../../db/client.ts'
5
5
  import { getLogger } from '../../logger.ts'
@@ -42,9 +42,26 @@ const vitalsCmd = health.sub('vitals')
42
42
  }
43
43
  })
44
44
 
45
+ const statsCmd = health.sub('stats')
46
+ .meta({ description: 'Lightweight usage stats' })
47
+ .run(async ({ flags }) => {
48
+ await initCliCommand('health stats', { debug: flags.debug, config: flags.config })
49
+ log.debug('Running health stats command')
50
+ const config = await loadConfig(process.cwd(), flags.config)
51
+ let db: Surreal | undefined
52
+ try {
53
+ db = await connect(config.surreal)
54
+ const s = await suemoStats(db)
55
+ console.log(JSON.stringify(s, null, 2))
56
+ } finally {
57
+ if (db) await disconnect()
58
+ }
59
+ })
60
+
45
61
  export const healthCmd = health
46
62
  .command(reportCmd)
47
63
  .command(vitalsCmd)
64
+ .command(statsCmd)
48
65
  // Default: run the report when just `suemo health` is called
49
66
  .run(async ({ flags }) => {
50
67
  await initCliCommand('health', { debug: flags.debug, config: flags.config })
@@ -44,6 +44,7 @@ export const observeCmd = app.sub('observe')
44
44
  scope: flags.scope,
45
45
  confidence: flags.confidence,
46
46
  source: flags.source,
47
+ sessionId: flags.session,
47
48
  }, config)
48
49
  console.log(JSON.stringify({ id: node.id, kind: node.kind, valid_from: node.valid_from }, null, 2))
49
50
  } finally {
@@ -1,20 +1,92 @@
1
- import { loadConfig } from '../../config.ts'
1
+ import { loadConfig, resolveSyncConfig } from '../../config.ts'
2
2
  import { getLogger } from '../../logger.ts'
3
3
  import { startMcpServer, startMcpStdioServer } from '../../mcp/server.ts'
4
4
  import { app, initCliCommand } from '../shared.ts'
5
5
 
6
6
  const log = getLogger(['suemo', 'cli', 'serve'])
7
7
 
8
+ function printDevRestartBanner(): void {
9
+ if (process.env.SUEMO_DEV_WATCH !== '1') return
10
+ const now = new Date().toISOString()
11
+ console.log(`\n[suemo:dev] MCP restarted (${now})\n`)
12
+ }
13
+
14
+ async function runServeDevMode(): Promise<never> {
15
+ const scriptPath = process.argv[1] ?? 'src/cli/index.ts'
16
+ const forwardedArgs = process.argv.slice(2).filter((arg) => arg !== '--dev')
17
+ const bunExecutable = process.execPath.includes('bun') ? process.execPath : 'bun'
18
+ const cmd = [bunExecutable, '--watch', scriptPath, ...forwardedArgs]
19
+
20
+ log.info('Starting serve dev mode (bun --watch)', {
21
+ scriptPath,
22
+ forwardedArgs,
23
+ })
24
+
25
+ const child = Bun.spawn({
26
+ cmd,
27
+ env: { ...process.env, SUEMO_DEV_WATCH: '1' },
28
+ stdin: 'inherit',
29
+ stdout: 'inherit',
30
+ stderr: 'inherit',
31
+ })
32
+
33
+ const shutdown = (signal: NodeJS.Signals): void => {
34
+ log.info('Stopping dev watcher', { signal })
35
+ try {
36
+ child.kill(signal)
37
+ } catch {
38
+ // noop
39
+ }
40
+ }
41
+
42
+ process.once('SIGINT', () => {
43
+ shutdown('SIGINT')
44
+ })
45
+ process.once('SIGTERM', () => {
46
+ shutdown('SIGTERM')
47
+ })
48
+
49
+ const exitCode = await child.exited
50
+ process.exit(exitCode)
51
+ }
52
+
53
+ async function maybeDelayDevStartup(): Promise<void> {
54
+ if (process.env.SUEMO_DEV_WATCH !== '1') return
55
+ const rawDelay = process.env.SUEMO_DEV_RESTART_DELAY_MS ?? '250'
56
+ const parsed = Number(rawDelay)
57
+ const delayMs = Number.isFinite(parsed) && parsed > 0 ? parsed : 250
58
+ if (delayMs > 0) {
59
+ log.debug('Delaying startup for dev restart stability', { delayMs })
60
+ await Bun.sleep(delayMs)
61
+ }
62
+ }
63
+
8
64
  export const serveCmd = app.sub('serve')
9
65
  .meta({ description: 'Start the MCP server (HTTP or stdio)' })
10
66
  .flags({
11
67
  port: { type: 'number', short: 'p', description: 'Port to listen on (overrides config)' },
12
68
  host: { type: 'string', description: 'Host to bind to (overrides config)' },
13
69
  stdio: { type: 'boolean', description: 'Use stdio transport instead of HTTP' },
70
+ dev: { type: 'boolean', description: 'Restart MCP server on code changes (bun --watch)' },
14
71
  })
15
72
  .run(async ({ flags }) => {
73
+ printDevRestartBanner()
74
+ await maybeDelayDevStartup()
16
75
  await initCliCommand('serve', { debug: flags.debug, config: flags.config })
76
+ if (flags.dev) {
77
+ await runServeDevMode()
78
+ }
17
79
  const config = await loadConfig(process.cwd(), flags.config)
80
+ const sync = resolveSyncConfig(config)
81
+ if (sync?.auto.enabled) {
82
+ log.info('Auto-sync enabled', {
83
+ remote: sync.auto.remote,
84
+ direction: sync.auto.direction,
85
+ intervalSeconds: sync.auto.intervalSeconds,
86
+ onWrite: sync.auto.onWrite,
87
+ minWriteIntervalSeconds: sync.auto.minWriteIntervalSeconds,
88
+ })
89
+ }
18
90
  if (flags.stdio) {
19
91
  log.debug('Starting MCP stdio transport')
20
92
  await startMcpStdioServer(config)
@@ -1,4 +1,4 @@
1
- import { loadConfig } from '../../config.ts'
1
+ import { loadConfig, resolveSyncConfig, type SurrealTarget } from '../../config.ts'
2
2
  import { connect, disconnect } from '../../db/client.ts'
3
3
  import { getLogger } from '../../logger.ts'
4
4
  import { syncTo } from '../../sync.ts'
@@ -7,24 +7,49 @@ import { app, initCliCommand } from '../shared.ts'
7
7
  const log = getLogger(['suemo', 'cli', 'sync'])
8
8
 
9
9
  export const syncCmd = app.sub('sync')
10
- .meta({ description: 'Push memories to remote SurrealDB (append-only)' })
10
+ .meta({ description: 'Sync memories with remote SurrealDB (push/pull/both)' })
11
11
  .flags({
12
12
  'dry-run': { type: 'boolean', description: 'Show what would be pushed without writing', default: false },
13
+ direction: { type: 'string', description: 'Sync direction: push | pull | both', default: 'push' },
14
+ remote: { type: 'string', description: 'Named remote from sync.remotes (defaults to sync.defaultRemote)' },
13
15
  })
14
16
  .run(async ({ flags }) => {
15
17
  await initCliCommand('sync', { debug: flags.debug, config: flags.config })
16
18
  const config = await loadConfig(process.cwd(), flags.config)
17
- if (!config.sync) {
18
- console.error('No sync.remote configured in suemo config.')
19
+ const sync = resolveSyncConfig(config)
20
+ if (!sync) {
21
+ console.error('No sync remotes configured. Set sync.remotes (or legacy sync.remote) in suemo config.')
19
22
  process.exit(1)
20
23
  }
24
+
25
+ const selectedRemoteName = flags.remote ?? sync.defaultRemote
26
+ const remote: SurrealTarget | undefined = sync.remotes[selectedRemoteName]
27
+ if (!remote) {
28
+ console.error(
29
+ `Unknown sync remote "${selectedRemoteName}". Available: ${Object.keys(sync.remotes).join(', ')}`,
30
+ )
31
+ process.exit(1)
32
+ }
33
+
21
34
  log.debug('Running sync command', {
22
35
  dryRun: flags['dry-run'],
23
- target: `${config.sync.remote.url}/${config.sync.remote.namespace}/${config.sync.remote.database}`,
36
+ direction: flags.direction,
37
+ remote: selectedRemoteName,
38
+ target: `${remote.url}/${remote.namespace}/${remote.database}`,
24
39
  })
40
+
41
+ const direction = flags.direction as 'push' | 'pull' | 'both'
42
+ if (!['push', 'pull', 'both'].includes(direction)) {
43
+ console.error(`Invalid direction "${flags.direction}". Use: push, pull, or both`)
44
+ process.exit(1)
45
+ }
46
+
25
47
  const db = await connect(config.surreal)
26
48
  try {
27
- const result = await syncTo(db, config.sync.remote, { dryRun: flags['dry-run'] })
49
+ const result = await syncTo(db, remote, {
50
+ dryRun: flags['dry-run'],
51
+ direction,
52
+ })
28
53
  console.log(JSON.stringify(result, null, 2))
29
54
  } finally {
30
55
  await disconnect()
@@ -33,6 +33,10 @@ export const wanderCmd = app.sub('wander')
33
33
  if (flags.json) {
34
34
  console.log(JSON.stringify(nodes, null, 2))
35
35
  } else {
36
+ if (nodes.length === 0) {
37
+ console.log('No memories found for this wander query.')
38
+ return
39
+ }
36
40
  for (const n of nodes) {
37
41
  console.log(`[${n.kind}] ${n.id} salience=${n.salience.toFixed(2)}`)
38
42
  console.log(` ${n.content.slice(0, 120)}`)
@@ -77,22 +77,16 @@ async function runNREM(
77
77
  // Find similar unassigned nodes using DB-side cosine
78
78
  const similarResult = await db.query<[{ id: string; score: number }[]]>(
79
79
  `
80
- LET $cand = (
81
- SELECT id, embedding
80
+ SELECT id, score
81
+ FROM (
82
+ SELECT id, vector::similarity::cosine(embedding, $emb) AS score
82
83
  FROM memory
83
84
  WHERE consolidated = false
84
85
  AND id != $self
85
86
  AND (valid_until = NONE OR valid_until > time::now())
86
- AND embedding <|10, 20|> $emb
87
- );
88
-
89
- SELECT id, score
90
- FROM (
91
- SELECT id, vector::similarity::cosine(embedding, $emb) AS score
92
- FROM $cand
93
87
  )
94
88
  ORDER BY score DESC
95
- LIMIT 10
89
+ LIMIT 500
96
90
  `,
97
91
  { emb: node.embedding, self: node.id },
98
92
  )
@@ -238,13 +232,10 @@ async function runREM(
238
232
  const candidates = await db.query<[MemoryNode[]]>(
239
233
  `
240
234
  SELECT * FROM memory
241
- WHERE id IN (
242
- SELECT VALUE id FROM memory
243
- WHERE id != $self
244
- AND consolidated = true
245
- AND (valid_until = NONE OR valid_until > time::now())
246
- AND embedding <|10, 40|> $emb
247
- )
235
+ WHERE id != $self
236
+ AND consolidated = true
237
+ AND (valid_until = NONE OR valid_until > time::now())
238
+ ORDER BY vector::similarity::cosine(embedding, $emb) DESC
248
239
  LIMIT 10
249
240
  `,
250
241
  { self: node.id, emb: node.embedding },
@@ -21,21 +21,15 @@ export async function detectContradiction(
21
21
 
22
22
  log.debug('detectContradiction()', { nodeId: newNode.id })
23
23
 
24
- const candidates = await db.query<[{ id: string; score: number }[]]>(
24
+ const candidates = await db.query<unknown[]>(
25
25
  `
26
- LET $cand = (
27
- SELECT id, embedding
26
+ SELECT id, score
27
+ FROM (
28
+ SELECT id, vector::similarity::cosine(embedding, $emb) AS score
28
29
  FROM memory
29
30
  WHERE kind = 'belief'
30
31
  AND id != $self
31
32
  AND (valid_until = NONE OR valid_until > time::now())
32
- AND embedding <|3, 20|> $emb
33
- );
34
-
35
- SELECT id, score
36
- FROM (
37
- SELECT id, vector::similarity::cosine(embedding, $emb) AS score
38
- FROM $cand
39
33
  )
40
34
  ORDER BY score DESC
41
35
  LIMIT 3
@@ -43,7 +37,7 @@ export async function detectContradiction(
43
37
  { emb: newNode.embedding, self: newNode.id },
44
38
  )
45
39
 
46
- const top = candidates[0]
40
+ const top = candidates.at(-1) as { id: string; score: number }[] | undefined
47
41
  if (!top || top.length === 0) return null
48
42
 
49
43
  const best = top[0]!
@@ -1,7 +1,7 @@
1
1
  import type { Surreal } from 'surrealdb'
2
2
  import { checkCompatibility } from '../db/preflight.ts'
3
3
  import { getLogger } from '../logger.ts'
4
- import type { ConsolidationRun, HealthReport } from '../types.ts'
4
+ import type { ConsolidationRun, HealthReport, SuemoStats } from '../types.ts'
5
5
 
6
6
  const log = getLogger(['suemo', 'cognitive', 'health'])
7
7
 
@@ -121,3 +121,63 @@ export async function vitals(db: Surreal): Promise<{
121
121
  nodesByScope,
122
122
  }
123
123
  }
124
+
125
+ export async function suemoStats(db: Surreal): Promise<SuemoStats> {
126
+ log.info('suemoStats()')
127
+
128
+ const [totalR, activeR, kindR, relR, runR, statsR] = await Promise.all([
129
+ db.query<[{ count: number }[]]>('SELECT count() AS count FROM memory GROUP ALL'),
130
+ db.query<[{ count: number }[]]>(
131
+ 'SELECT count() AS count FROM memory WHERE valid_until = NONE OR valid_until > time::now() GROUP ALL',
132
+ ),
133
+ db.query<[{ kind: string; count: number }[]]>('SELECT kind, count() AS count FROM memory GROUP BY kind'),
134
+ db.query<[{ count: number }[]]>('SELECT count() AS count FROM relates_to GROUP ALL'),
135
+ db.query<[{ count: number }[]]>('SELECT count() AS count FROM consolidation_run GROUP ALL'),
136
+ db.query<[
137
+ { total_writes: number; total_queries: number; last_write: string | null; last_query: string | null }[],
138
+ ]>(
139
+ 'SELECT total_writes, total_queries, last_write, last_query FROM suemo_stats:default LIMIT 1',
140
+ ),
141
+ ])
142
+
143
+ const byKind: Record<string, number> = {}
144
+ for (const r of kindR[0] ?? []) byKind[r.kind] = r.count
145
+
146
+ const s = statsR[0]?.[0]
147
+
148
+ return {
149
+ totalNodes: totalR[0]?.[0]?.count ?? 0,
150
+ activeNodes: activeR[0]?.[0]?.count ?? 0,
151
+ nodesByKind: byKind,
152
+ relations: relR[0]?.[0]?.count ?? 0,
153
+ consolidationRuns: runR[0]?.[0]?.count ?? 0,
154
+ lastWrite: s?.last_write ?? null,
155
+ lastQuery: s?.last_query ?? null,
156
+ totalWrites: s?.total_writes ?? 0,
157
+ totalQueries: s?.total_queries ?? 0,
158
+ }
159
+ }
160
+
161
+ export async function incrementWriteStats(db: Surreal): Promise<void> {
162
+ await db.query(
163
+ `
164
+ UPSERT suemo_stats:default SET
165
+ ns_db = $nsDb,
166
+ total_writes = IF total_writes = NONE THEN 1 ELSE total_writes + 1 END,
167
+ last_write = time::now()
168
+ `,
169
+ { nsDb: 'default' },
170
+ )
171
+ }
172
+
173
+ export async function incrementQueryStats(db: Surreal): Promise<void> {
174
+ await db.query(
175
+ `
176
+ UPSERT suemo_stats:default SET
177
+ ns_db = $nsDb,
178
+ total_queries = IF total_queries = NONE THEN 1 ELSE total_queries + 1 END,
179
+ last_query = time::now()
180
+ `,
181
+ { nsDb: 'default' },
182
+ )
183
+ }
@@ -33,4 +33,26 @@ export default defineConfig({
33
33
  port: Number(process.env.SUEMO_PORT) || 4242,
34
34
  host: '127.0.0.1',
35
35
  },
36
+ sync: {
37
+ remotes: {
38
+ vps: {
39
+ url: process.env.SUEMO_SYNC_URL ?? process.env.SUEMO_URL ?? 'ws://localhost:8000',
40
+ namespace: process.env.SUEMO_SYNC_NS ?? process.env.SUEMO_NS ?? 'suemo',
41
+ database: process.env.SUEMO_SYNC_DB ?? process.env.SUEMO_DB ?? 'suemo',
42
+ auth: {
43
+ user: process.env.SUEMO_SYNC_USER ?? process.env.SUEMO_USER ?? 'root',
44
+ pass: process.env.SUEMO_SYNC_PASS ?? process.env.SUEMO_PASS ?? 'pass',
45
+ },
46
+ },
47
+ },
48
+ defaultRemote: 'vps',
49
+ auto: {
50
+ enabled: false,
51
+ intervalSeconds: 300,
52
+ direction: 'push',
53
+ remote: 'vps',
54
+ onWrite: false,
55
+ minWriteIntervalSeconds: 30,
56
+ },
57
+ },
36
58
  })