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.
@@ -0,0 +1,123 @@
1
+ import type { Surreal } from 'surrealdb'
2
+ import { checkCompatibility } from '../db/preflight.ts'
3
+ import { getLogger } from '../logger.ts'
4
+ import type { ConsolidationRun, HealthReport } from '../types.ts'
5
+
6
+ const log = getLogger(['suemo', 'cognitive', 'health'])
7
+
8
+ export async function healthReport(db: Surreal): Promise<HealthReport> {
9
+ log.info('healthReport()')
10
+
11
+ const [
12
+ totalResult,
13
+ activeResult,
14
+ consolidatedResult,
15
+ byKindResult,
16
+ byScopeResult,
17
+ relationCountResult,
18
+ activeGoalsResult,
19
+ frssDueResult,
20
+ lastRunResult,
21
+ compatResult,
22
+ ] = await Promise.all([
23
+ // total nodes
24
+ db.query<[{ count: number }[]]>('SELECT count() AS count FROM memory GROUP ALL'),
25
+ // active nodes
26
+ db.query<[{ count: number }[]]>(`
27
+ SELECT count() AS count FROM memory
28
+ WHERE valid_until = NONE OR valid_until > time::now()
29
+ GROUP ALL
30
+ `),
31
+ // consolidated nodes
32
+ db.query<[{ count: number }[]]>(
33
+ 'SELECT count() AS count FROM memory WHERE consolidated = true GROUP ALL',
34
+ ),
35
+ // by kind
36
+ db.query<[{ kind: string; count: number }[]]>(
37
+ 'SELECT kind, count() AS count FROM memory GROUP BY kind',
38
+ ),
39
+ // by scope
40
+ db.query<[{ scope: string | null; count: number }[]]>(
41
+ 'SELECT scope, count() AS count FROM memory WHERE scope != NONE GROUP BY scope',
42
+ ),
43
+ // relation count
44
+ db.query<[{ count: number }[]]>('SELECT count() AS count FROM relates_to GROUP ALL'),
45
+ // active goals
46
+ db.query<[{ count: number }[]]>(`
47
+ SELECT count() AS count FROM memory
48
+ WHERE kind = 'goal' AND (valid_until = NONE OR valid_until > time::now())
49
+ GROUP ALL
50
+ `),
51
+ // FSRS due
52
+ db.query<[{ count: number }[]]>(`
53
+ SELECT count() AS count FROM memory
54
+ WHERE fsrs_next_review != NONE
55
+ AND fsrs_next_review <= time::now()
56
+ AND (valid_until = NONE OR valid_until > time::now())
57
+ GROUP ALL
58
+ `),
59
+ // last consolidation run
60
+ db.query<[ConsolidationRun[]]>(
61
+ 'SELECT * FROM consolidation_run ORDER BY started_at DESC LIMIT 1',
62
+ ),
63
+ // compat check (non-blocking)
64
+ checkCompatibility(db),
65
+ ])
66
+
67
+ const byKind: Record<string, number> = {}
68
+ for (const row of byKindResult[0] ?? []) byKind[row.kind] = row.count
69
+
70
+ const byScope: Record<string, number> = {}
71
+ for (const row of byScopeResult[0] ?? []) {
72
+ if (row.scope) byScope[row.scope] = row.count
73
+ }
74
+
75
+ return {
76
+ nodes: {
77
+ total: totalResult[0]?.[0]?.count ?? 0,
78
+ active: activeResult[0]?.[0]?.count ?? 0,
79
+ consolidated: consolidatedResult[0]?.[0]?.count ?? 0,
80
+ by_kind: byKind,
81
+ by_scope: byScope,
82
+ },
83
+ relations: relationCountResult[0]?.[0]?.count ?? 0,
84
+ goals_active: activeGoalsResult[0]?.[0]?.count ?? 0,
85
+ fsrs_due: frssDueResult[0]?.[0]?.count ?? 0,
86
+ last_consolidation: lastRunResult[0]?.[0] ?? null,
87
+ version_check: {
88
+ surreal_version: compatResult.surrealVersion,
89
+ surrealkv: compatResult.surrealkv,
90
+ retention_ok: compatResult.retention_ok,
91
+ },
92
+ }
93
+ }
94
+
95
+ export async function vitals(db: Surreal): Promise<{
96
+ last10Runs: ConsolidationRun[]
97
+ nodesByKind: Record<string, number>
98
+ nodesByScope: Record<string, number>
99
+ }> {
100
+ log.info('vitals()')
101
+ const [runsResult, byKindResult, byScopeResult] = await Promise.all([
102
+ db.query<[ConsolidationRun[]]>(
103
+ 'SELECT * FROM consolidation_run ORDER BY started_at DESC LIMIT 10',
104
+ ),
105
+ db.query<[{ kind: string; count: number }[]]>(
106
+ 'SELECT kind, count() AS count FROM memory GROUP BY kind',
107
+ ),
108
+ db.query<[{ scope: string | null; count: number }[]]>(
109
+ 'SELECT scope, count() AS count FROM memory WHERE scope != NONE GROUP BY scope',
110
+ ),
111
+ ])
112
+
113
+ const nodesByKind: Record<string, number> = {}
114
+ for (const r of byKindResult[0] ?? []) nodesByKind[r.kind] = r.count
115
+ const nodesByScope: Record<string, number> = {}
116
+ for (const r of byScopeResult[0] ?? []) if (r.scope) nodesByScope[r.scope] = r.count
117
+
118
+ return {
119
+ last10Runs: runsResult[0] ?? [],
120
+ nodesByKind,
121
+ nodesByScope,
122
+ }
123
+ }
package/src/config.ts ADDED
@@ -0,0 +1,114 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join, resolve } from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ // ── Config shape ──────────────────────────────────────────────────────────────
6
+
7
+ export interface AuthConfig {
8
+ user: string
9
+ pass: string
10
+ }
11
+
12
+ export interface SurrealTarget {
13
+ url: string
14
+ namespace: string
15
+ database: string
16
+ auth: AuthConfig
17
+ }
18
+
19
+ export type EmbeddingProvider =
20
+ | { provider: 'surreal'; dimension: number }
21
+ | { provider: 'stub'; dimension: number } // test-only
22
+
23
+ export interface LLMConfig {
24
+ url: string // OpenAI-compatible endpoint
25
+ model: string
26
+ apiKey: string
27
+ }
28
+
29
+ export interface ConsolidationConfig {
30
+ trigger: 'timer' | 'reactive' | 'manual'
31
+ intervalMinutes: number
32
+ reactiveThreshold: number // unconsolidated node count before auto-trigger
33
+ nremSimilarityThreshold: number // cosine threshold for clustering (0.0–1.0)
34
+ remRelationThreshold: number // min score to create an auto-relation (0.0–1.0)
35
+ llm: LLMConfig
36
+ }
37
+
38
+ export interface RetrievalConfig {
39
+ weights: {
40
+ vector: number
41
+ bm25: number
42
+ graph: number
43
+ temporal: number
44
+ }
45
+ }
46
+
47
+ export interface McpConfig {
48
+ port: number
49
+ host: string
50
+ }
51
+
52
+ export interface SyncConfig {
53
+ remote: SurrealTarget
54
+ cursor?: string // ISO datetime; absence means full sync on first run
55
+ }
56
+
57
+ export interface SuemoConfig {
58
+ surreal: SurrealTarget
59
+ embedding: EmbeddingProvider
60
+ consolidation: ConsolidationConfig
61
+ retrieval: RetrievalConfig
62
+ mcp: McpConfig
63
+ sync?: SyncConfig
64
+ }
65
+
66
+ // ── defineConfig — identity fn for type safety ─────────────────────────────
67
+
68
+ export function defineConfig(config: SuemoConfig): SuemoConfig {
69
+ return config
70
+ }
71
+
72
+ // ── loadConfig — resolution chain ────────────────────────────────────────────
73
+
74
+ const CONFIG_CANDIDATES = [
75
+ 'suemo.config.ts',
76
+ 'suemo.config.js',
77
+ ]
78
+
79
+ const HOME_CONFIG = join(
80
+ process.env.HOME ?? process.env.USERPROFILE ?? '~',
81
+ '.suemo',
82
+ 'suemo.ts',
83
+ )
84
+
85
+ export async function loadConfig(
86
+ cwd = process.cwd(),
87
+ overridePath?: string,
88
+ ): Promise<SuemoConfig> {
89
+ // 1. Explicit --config flag
90
+ if (overridePath) {
91
+ return importConfig(resolve(overridePath))
92
+ }
93
+ // 2. Project-local
94
+ for (const name of CONFIG_CANDIDATES) {
95
+ const p = resolve(cwd, name)
96
+ if (existsSync(p)) return importConfig(p)
97
+ }
98
+ // 3. User-level
99
+ if (existsSync(HOME_CONFIG)) return importConfig(HOME_CONFIG)
100
+
101
+ throw new Error(
102
+ 'No suemo config found.\n'
103
+ + 'Run `suemo init` to create ~/.suemo/suemo.ts, or create suemo.config.ts in the project root.',
104
+ )
105
+ }
106
+
107
+ async function importConfig(path: string): Promise<SuemoConfig> {
108
+ const mod = await import(pathToFileURL(path).href)
109
+ const cfg: unknown = mod.default ?? mod
110
+ if (!cfg || typeof cfg !== 'object') {
111
+ throw new Error(`Config at ${path} does not export a default object`)
112
+ }
113
+ return cfg as SuemoConfig // trust defineConfig() for now; add Zod parse if needed
114
+ }
@@ -0,0 +1,59 @@
1
+ // SDK v2: requires @surrealdb/node for WebSocket support in Bun/Node runtimes.
2
+ // createRemoteEngines() handles ws://, wss://, http://, https:// connections.
3
+ // createNodeEngines() patches in the Node.js WebSocket implementation.
4
+ import { createNodeEngines } from '@surrealdb/node'
5
+ import { createRemoteEngines, Surreal } from 'surrealdb'
6
+ import type { SurrealTarget } from '../config.ts'
7
+ import { getLogger } from '../logger.ts'
8
+
9
+ const log = getLogger(['suemo', 'db', 'client'])
10
+
11
+ let _db: Surreal | null = null
12
+
13
+ export async function connect(target: SurrealTarget): Promise<Surreal> {
14
+ if (_db) return _db
15
+
16
+ log.info('Connecting to SurrealDB', {
17
+ url: target.url,
18
+ ns: target.namespace,
19
+ db: target.database,
20
+ })
21
+
22
+ // v2 SDK: engines are registered at construction time, not at connect.
23
+ // Both remote (ws/wss/http) and Node.js engines are needed for Bun.
24
+ const db = new Surreal({
25
+ engines: {
26
+ ...createRemoteEngines(),
27
+ ...createNodeEngines(),
28
+ },
29
+ })
30
+
31
+ // v2 SDK: namespace, database, and authentication can all be passed inline
32
+ // to connect(). The `authentication` callback is invoked on connect and on
33
+ // any automatic reconnection/token-refresh cycle.
34
+ await db.connect(target.url, {
35
+ namespace: target.namespace,
36
+ database: target.database,
37
+ authentication: () => ({
38
+ username: target.auth.user,
39
+ password: target.auth.pass,
40
+ }),
41
+ })
42
+
43
+ _db = db
44
+ log.info('Connected')
45
+ return db
46
+ }
47
+
48
+ export function getDb(): Surreal {
49
+ if (!_db) throw new Error('SurrealDB not connected. Call connect() first.')
50
+ return _db
51
+ }
52
+
53
+ export async function disconnect(): Promise<void> {
54
+ if (_db) {
55
+ await _db.close()
56
+ _db = null
57
+ log.info('Disconnected')
58
+ }
59
+ }
@@ -0,0 +1,152 @@
1
+ import type { Surreal } from 'surrealdb'
2
+ import { getLogger } from '../logger.ts'
3
+
4
+ const log = getLogger(['suemo', 'db', 'preflight'])
5
+
6
+ export interface CompatibilityResult {
7
+ ok: boolean
8
+ surrealVersion: string
9
+ surrealkv: boolean
10
+ retention_ok: boolean // SURREAL_DATASTORE_RETENTION on server >= 90d
11
+ embedding: boolean
12
+ errors: string[]
13
+ }
14
+
15
+ const MIN_MAJOR = 3
16
+
17
+ export async function checkCompatibility(
18
+ db: Surreal,
19
+ ): Promise<CompatibilityResult> {
20
+ const errors: string[] = []
21
+ let surrealVersion = 'unknown'
22
+ let surrealkv = false
23
+ let retention_ok = false
24
+ let embedding = false
25
+
26
+ log.info('Running preflight compatibility checks')
27
+
28
+ // ── Check 1: version string ───────────────────────────────────────────────
29
+ try {
30
+ surrealVersion = (await db.version()).version
31
+ // version string: "surrealdb-3.0.4" → extract semver
32
+ const match = surrealVersion.match(/(\d+)\.(\d+)\.(\d+)/)
33
+ if (!match) {
34
+ errors.push(`Cannot parse version string: "${surrealVersion}"`)
35
+ } else {
36
+ const major = parseInt(match[1]!, 10)
37
+ if (major < MIN_MAJOR) {
38
+ errors.push(
39
+ `SurrealDB ${surrealVersion} is below the required v${MIN_MAJOR}.0.0. `
40
+ + `Start suemo with: surreal start --bind 0.0.0.0:8000 surrealkv://path/to/data`,
41
+ )
42
+ }
43
+ }
44
+ } catch (e) {
45
+ errors.push(`Cannot retrieve SurrealDB version: ${String(e)}`)
46
+ }
47
+
48
+ // ── Check 2: SurrealKV (VERSION keyword probe) ────────────────────────────
49
+ // We CREATE a sentinel record, query it with VERSION, then DELETE it.
50
+ // If VERSION errors, the storage engine is not SurrealKV.
51
+ try {
52
+ await db.query('CREATE suemo_preflight:probe SET checked_at = time::now()')
53
+ // VERSION at a past datetime should return an empty result set (or the record
54
+ // if it was around then), not throw. An error means VERSION is unsupported.
55
+ await db.query("SELECT * FROM suemo_preflight:probe VERSION d'2020-01-01T00:00:00Z'")
56
+ surrealkv = true
57
+ } catch (e: unknown) {
58
+ const msg = String(e).toLowerCase()
59
+ if (msg.includes('version') || msg.includes('surrealkv') || msg.includes('not supported')) {
60
+ errors.push(
61
+ 'VERSION queries not supported — SurrealDB must be started with SurrealKV storage. '
62
+ + 'Use: surreal start surrealkv://path/to/data',
63
+ )
64
+ } else {
65
+ // Unexpected error — still fail but report verbatim
66
+ errors.push(`SurrealKV probe failed unexpectedly: ${String(e)}`)
67
+ }
68
+ } finally {
69
+ try {
70
+ await db.query('DELETE suemo_preflight:probe')
71
+ } catch { /* ignore cleanup errors */ }
72
+ }
73
+
74
+ // ── Check 3: SURREAL_DATASTORE_RETENTION — probe VERSION reach ──────────────
75
+ // We can't read env vars from the server directly via SurrealQL. Instead we
76
+ // probe how far back VERSION queries can reach using the sentinel record we
77
+ // already created above (or a second probe). If VERSION 90 days ago resolves
78
+ // without a "retention" error, the server has sufficient retention configured.
79
+ // Note: SurrealKV has no hard deletes yet — retention controls time-travel depth.
80
+ if (surrealkv) {
81
+ try {
82
+ const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString()
83
+ await db.query('CREATE suemo_retention_probe:v SET t = time::now()')
84
+ await db.query(`SELECT * FROM suemo_retention_probe:v VERSION d'${ninetyDaysAgo}'`)
85
+ retention_ok = true
86
+ } catch (e: unknown) {
87
+ const msg = String(e).toLowerCase()
88
+ if (msg.includes('retention') || msg.includes('version')) {
89
+ errors.push(
90
+ 'SurrealDB VERSION time-travel cannot reach 90 days back. '
91
+ + 'Start SurrealDB with: SURREAL_DATASTORE_RETENTION=90d surreal start surrealkv://path/to/data',
92
+ )
93
+ } else {
94
+ // Unexpected error — still flag but don't assume retention is wrong
95
+ retention_ok = true
96
+ log.debug('Retention probe returned unexpected error (may be fine)', { error: String(e) })
97
+ }
98
+ } finally {
99
+ try {
100
+ await db.query('DELETE suemo_retention_probe:v')
101
+ } catch { /* ignore */ }
102
+ }
103
+ }
104
+
105
+ // ── Check 4: fn::embed() resolves ────────────────────────────────────────
106
+ // We don't actually embed anything — we just check that the function exists.
107
+ // An "Unknown function" error means the embedding plugin is not configured.
108
+ try {
109
+ // fn::embed requires the ML module and a configured model.
110
+ // If it throws "No embedding model configured" that's acceptable — the
111
+ // function exists. If it throws "Unknown function 'fn::embed'" → not available.
112
+ await db.query(`RETURN fn::embed("suemo preflight test")`)
113
+ embedding = true
114
+ } catch (e: unknown) {
115
+ const msg = String(e).toLowerCase()
116
+ if (msg.includes('unknown function') || msg.includes('fn::embed')) {
117
+ errors.push(
118
+ 'fn::embed() is not available. Configure an embedding model in SurrealDB, or '
119
+ + "use embedding.provider = 'stub' in suemo config (test-only).",
120
+ )
121
+ } else {
122
+ // Other error (e.g. no model configured but function exists) — treat as available
123
+ embedding = true
124
+ log.debug('fn::embed() exists but returned an error (model may not be configured yet)', { error: String(e) })
125
+ }
126
+ }
127
+
128
+ const ok = errors.length === 0
129
+
130
+ if (ok) {
131
+ log.info('All preflight checks passed', { surrealVersion, surrealkv, retention_ok, embedding })
132
+ } else {
133
+ log.error('Preflight checks failed', { errors })
134
+ }
135
+
136
+ return { ok, surrealVersion, surrealkv, retention_ok, embedding, errors }
137
+ }
138
+
139
+ /**
140
+ * Hard-exit variant. Call in CLI commands; throw in MCP server startup.
141
+ */
142
+ export async function requireCompatibility(db: Surreal): Promise<void> {
143
+ const result = await checkCompatibility(db)
144
+ if (!result.ok) {
145
+ console.error('\n[suemo] Compatibility check failed:\n')
146
+ for (const err of result.errors) {
147
+ console.error(` ✗ ${err}`)
148
+ }
149
+ console.error('\nFix the issues above and retry.\n')
150
+ process.exit(1)
151
+ }
152
+ }
@@ -0,0 +1,109 @@
1
+ -- ─────────────────────────────────────────────────────────────────────────────
2
+ -- suemo schema — SurrealDB v3.0 + SurrealKV
3
+ -- All DEFINE statements use OVERWRITE so this is idempotent.
4
+ -- ─────────────────────────────────────────────────────────────────────────────
5
+
6
+ -- ── memory ───────────────────────────────────────────────────────────────────
7
+ DEFINE TABLE OVERWRITE memory SCHEMAFULL;
8
+
9
+ DEFINE FIELD OVERWRITE kind ON memory TYPE string
10
+ ASSERT $value INSIDE ['observation','belief','question','hypothesis','goal'];
11
+
12
+ DEFINE FIELD OVERWRITE content ON memory TYPE string;
13
+ DEFINE FIELD OVERWRITE summary ON memory TYPE option<string>;
14
+ DEFINE FIELD OVERWRITE tags ON memory TYPE set<string> DEFAULT {};
15
+ DEFINE FIELD OVERWRITE scope ON memory TYPE option<string>;
16
+ DEFINE FIELD OVERWRITE embedding ON memory TYPE array<float> DEFAULT [];
17
+ DEFINE FIELD OVERWRITE confidence ON memory TYPE float DEFAULT 1.0;
18
+ DEFINE FIELD OVERWRITE salience ON memory TYPE float DEFAULT 0.5;
19
+
20
+ -- Temporal: valid_from always set by system at write time
21
+ -- valid_until: set by system on contradiction detection or explicit invalidate()
22
+ -- Agents NEVER supply either field.
23
+ DEFINE FIELD OVERWRITE valid_from ON memory TYPE datetime DEFAULT time::now();
24
+ DEFINE FIELD OVERWRITE valid_until ON memory TYPE option<datetime> DEFAULT NONE;
25
+
26
+ DEFINE FIELD OVERWRITE source ON memory TYPE option<string>;
27
+ DEFINE FIELD OVERWRITE created_at ON memory TYPE datetime DEFAULT time::now();
28
+ DEFINE FIELD OVERWRITE updated_at ON memory TYPE datetime DEFAULT time::now();
29
+ DEFINE FIELD OVERWRITE consolidated ON memory TYPE bool DEFAULT false;
30
+
31
+ -- REFERENCES: bidirectional record link (SurrealDB v3.0)
32
+ DEFINE FIELD OVERWRITE consolidated_into ON memory
33
+ TYPE option<record<memory>> REFERENCES;
34
+
35
+ -- FSRS spaced-repetition fields (all optional; populated on first recall())
36
+ DEFINE FIELD OVERWRITE fsrs_stability ON memory TYPE option<float>;
37
+ DEFINE FIELD OVERWRITE fsrs_difficulty ON memory TYPE option<float>;
38
+ DEFINE FIELD OVERWRITE fsrs_next_review ON memory TYPE option<datetime>;
39
+
40
+ -- Vector index: HNSW, DEFER so init does not block on large existing datasets
41
+ DEFINE INDEX OVERWRITE idx_memory_embedding
42
+ ON memory FIELDS embedding
43
+ HNSW DIMENSION 1536 DIST COSINE
44
+ DEFER;
45
+
46
+ -- Full-text index: BM25 over content + summary
47
+ DEFINE ANALYZER OVERWRITE suemo_analyzer
48
+ TOKENIZERS blank,class
49
+ FILTERS lowercase,snowball(english);
50
+
51
+ DEFINE INDEX OVERWRITE idx_memory_fts
52
+ ON memory FIELDS content, summary
53
+ SEARCH ANALYZER suemo_analyzer BM25;
54
+
55
+ -- Compound indexes for timeline and filter queries
56
+ DEFINE INDEX OVERWRITE idx_memory_scope_time
57
+ ON memory FIELDS scope, created_at;
58
+
59
+ DEFINE INDEX OVERWRITE idx_memory_kind_valid
60
+ ON memory FIELDS kind, valid_until;
61
+
62
+ DEFINE INDEX OVERWRITE idx_memory_salience
63
+ ON memory FIELDS salience;
64
+
65
+ -- ── relates_to ───────────────────────────────────────────────────────────────
66
+ DEFINE TABLE OVERWRITE relates_to SCHEMAFULL
67
+ TYPE RELATION IN memory OUT memory;
68
+
69
+ DEFINE FIELD OVERWRITE kind ON relates_to TYPE string
70
+ ASSERT $value INSIDE ['supports','contradicts','derived_from','caused_by','similar_to','updates','sequential'];
71
+
72
+ DEFINE FIELD OVERWRITE strength ON relates_to TYPE float DEFAULT 0.5;
73
+ DEFINE FIELD OVERWRITE valid_from ON relates_to TYPE datetime DEFAULT time::now();
74
+ DEFINE FIELD OVERWRITE valid_until ON relates_to TYPE option<datetime> DEFAULT NONE;
75
+ DEFINE FIELD OVERWRITE created_at ON relates_to TYPE datetime DEFAULT time::now();
76
+
77
+ -- ── episode ───────────────────────────────────────────────────────────────────
78
+ DEFINE TABLE OVERWRITE episode SCHEMAFULL;
79
+
80
+ DEFINE FIELD OVERWRITE session_id ON episode TYPE string;
81
+ DEFINE FIELD OVERWRITE started_at ON episode TYPE datetime DEFAULT time::now();
82
+ DEFINE FIELD OVERWRITE ended_at ON episode TYPE option<datetime> DEFAULT NONE;
83
+ DEFINE FIELD OVERWRITE summary ON episode TYPE option<string>;
84
+
85
+ -- REFERENCES: bidirectional array of memory links
86
+ DEFINE FIELD OVERWRITE memory_ids ON episode
87
+ TYPE array<record<memory>> REFERENCES
88
+ DEFAULT [];
89
+
90
+ -- ── consolidation_run ────────────────────────────────────────────────────────
91
+ DEFINE TABLE OVERWRITE consolidation_run SCHEMAFULL;
92
+
93
+ DEFINE FIELD OVERWRITE started_at ON consolidation_run TYPE datetime DEFAULT time::now();
94
+ DEFINE FIELD OVERWRITE completed_at ON consolidation_run TYPE option<datetime> DEFAULT NONE;
95
+ DEFINE FIELD OVERWRITE phase ON consolidation_run TYPE string
96
+ ASSERT $value INSIDE ['nrem','rem','full'];
97
+ DEFINE FIELD OVERWRITE nodes_in ON consolidation_run TYPE int DEFAULT 0;
98
+ DEFINE FIELD OVERWRITE nodes_out ON consolidation_run TYPE int DEFAULT 0;
99
+ DEFINE FIELD OVERWRITE status ON consolidation_run TYPE string
100
+ ASSERT $value INSIDE ['running','done','failed'];
101
+ DEFINE FIELD OVERWRITE error ON consolidation_run TYPE option<string> DEFAULT NONE;
102
+
103
+ -- ── sync_cursor ───────────────────────────────────────────────────────────────
104
+ -- One record per (remote.url, remote.ns, remote.db) triple.
105
+ -- Stores the last successfully synced created_at timestamp.
106
+ DEFINE TABLE OVERWRITE sync_cursor SCHEMAFULL;
107
+ DEFINE FIELD OVERWRITE remote_key ON sync_cursor TYPE string; -- sha1(url+ns+db)
108
+ DEFINE FIELD OVERWRITE cursor ON sync_cursor TYPE datetime;
109
+ DEFINE FIELD OVERWRITE last_synced ON sync_cursor TYPE datetime DEFAULT time::now();
@@ -0,0 +1,24 @@
1
+ import { Surreal } from 'surrealdb'
2
+ import { getLogger } from '../logger.ts'
3
+
4
+ import SCHEMA from './schema.surql' with { type: 'text' }
5
+
6
+ const log = getLogger(['suemo', 'db', 'schema'])
7
+
8
+ // schema.surql is inlined as a template string to avoid filesystem read concerns
9
+ // at runtime. Each statement is separated by ";\n" and executed individually.
10
+
11
+ export async function runSchema(db: Surreal): Promise<void> {
12
+ log.info('Running schema migrations')
13
+ const statements = SCHEMA.split(/;\s*\n/).map((s) => s.trim()).filter(Boolean)
14
+ for (const stmt of statements) {
15
+ try {
16
+ await db.query(stmt)
17
+ log.debug('Schema statement OK', { stmt: stmt.slice(0, 60) })
18
+ } catch (e) {
19
+ log.error('Schema statement failed', { stmt, error: String(e) })
20
+ throw e
21
+ }
22
+ }
23
+ log.info('Schema ready')
24
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module '*.surql' {
2
+ const content: string
3
+ export default content
4
+ }
package/src/goal.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { Surreal } from 'surrealdb'
2
+ import { getLogger } from './logger.ts'
3
+ import { invalidate, observe } from './memory/write.ts'
4
+ import type { MemoryNode } from './types.ts'
5
+
6
+ const log = getLogger(['suemo', 'goal'])
7
+
8
+ export async function goalSet(
9
+ db: Surreal,
10
+ content: string,
11
+ opts: { scope?: string; tags?: string[] } = {},
12
+ ): Promise<MemoryNode> {
13
+ log.info('goalSet()', { content: content.slice(0, 60) })
14
+ return observe(db, { content, kind: 'goal', ...opts })
15
+ }
16
+
17
+ export async function goalResolve(db: Surreal, goalId: string): Promise<void> {
18
+ log.info('goalResolve()', { goalId })
19
+ await invalidate(db, goalId, 'resolved')
20
+ }
21
+
22
+ export async function goalList(
23
+ db: Surreal,
24
+ opts: { scope?: string; includeResolved?: boolean } = {},
25
+ ): Promise<MemoryNode[]> {
26
+ log.info('goalList()', opts)
27
+ const filter = opts.includeResolved
28
+ ? "kind = 'goal'"
29
+ : "kind = 'goal' AND (valid_until = NONE OR valid_until > time::now())"
30
+ return db.query<[MemoryNode[]]>(
31
+ `
32
+ SELECT * FROM memory
33
+ WHERE ${filter}
34
+ AND ($scope = NONE OR scope = $scope)
35
+ ORDER BY created_at DESC
36
+ `,
37
+ { scope: opts.scope ?? null },
38
+ ).then((r) => r[0] ?? [])
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ // src/index.ts — public API surface
2
+ export { defineConfig, loadConfig } from './config.ts'
3
+ export type {
4
+ AuthConfig,
5
+ ConsolidationConfig,
6
+ EmbeddingProvider,
7
+ LLMConfig,
8
+ McpConfig,
9
+ RetrievalConfig,
10
+ SuemoConfig,
11
+ SurrealTarget,
12
+ SyncConfig,
13
+ } from './config.ts'