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/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { join, resolve } from 'node:path'
3
3
  import { pathToFileURL } from 'node:url'
4
+ import { getLogger } from './logger.ts'
4
5
 
5
6
  // ── Config shape ──────────────────────────────────────────────────────────────
6
7
 
@@ -17,8 +18,9 @@ export interface SurrealTarget {
17
18
  }
18
19
 
19
20
  export type EmbeddingProvider =
21
+ | { provider: 'openai-compatible'; url: string; model: string; dimension: number; apiKey?: string }
20
22
  | { provider: 'surreal'; dimension: number }
21
- | { provider: 'stub'; dimension: number } // test-only
23
+ | { provider: 'stub'; dimension: number }
22
24
 
23
25
  export interface LLMConfig {
24
26
  url: string // OpenAI-compatible endpoint
@@ -49,9 +51,41 @@ export interface McpConfig {
49
51
  host: string
50
52
  }
51
53
 
54
+ export type SyncDirectionConfig = 'push' | 'pull' | 'both'
55
+
56
+ export interface SyncAutoConfig {
57
+ enabled?: boolean
58
+ intervalSeconds?: number
59
+ direction?: SyncDirectionConfig
60
+ remote?: string
61
+ onWrite?: boolean
62
+ minWriteIntervalSeconds?: number
63
+ }
64
+
52
65
  export interface SyncConfig {
53
- remote: SurrealTarget
54
- cursor?: string // ISO datetime; absence means full sync on first run
66
+ /** Legacy single-remote config (still supported for migration safety). */
67
+ remote?: SurrealTarget
68
+ /** Named remotes for selectable sync targets. */
69
+ remotes?: Record<string, SurrealTarget>
70
+ /** Default key from `remotes` when no explicit remote is selected. */
71
+ defaultRemote?: string
72
+ /** Optional auto-sync behavior for long-running MCP servers. */
73
+ auto?: SyncAutoConfig
74
+ }
75
+
76
+ export interface ResolvedSyncAutoConfig {
77
+ enabled: boolean
78
+ intervalSeconds: number
79
+ direction: SyncDirectionConfig
80
+ remote: string
81
+ onWrite: boolean
82
+ minWriteIntervalSeconds: number
83
+ }
84
+
85
+ export interface ResolvedSyncConfig {
86
+ remotes: Record<string, SurrealTarget>
87
+ defaultRemote: string
88
+ auto: ResolvedSyncAutoConfig
55
89
  }
56
90
 
57
91
  export interface SuemoConfig {
@@ -69,6 +103,55 @@ export function defineConfig(config: SuemoConfig): SuemoConfig {
69
103
  return config
70
104
  }
71
105
 
106
+ export function resolveSyncConfig(config: SuemoConfig): ResolvedSyncConfig | null {
107
+ if (!config.sync) return null
108
+
109
+ const remotes: Record<string, SurrealTarget> = config.sync.remotes
110
+ ? { ...config.sync.remotes }
111
+ : (config.sync.remote ? { default: config.sync.remote } : {})
112
+
113
+ const remoteNames = Object.keys(remotes)
114
+ if (remoteNames.length === 0) return null
115
+
116
+ const defaultRemote = config.sync.defaultRemote ?? remoteNames[0]!
117
+ if (!remotes[defaultRemote]) {
118
+ throw new Error(
119
+ `sync.defaultRemote \"${defaultRemote}\" does not exist in sync.remotes. Available: ${remoteNames.join(', ')}`,
120
+ )
121
+ }
122
+
123
+ const auto = config.sync.auto ?? {}
124
+ const intervalSeconds = auto.intervalSeconds ?? 300
125
+ const minWriteIntervalSeconds = auto.minWriteIntervalSeconds ?? 30
126
+
127
+ if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
128
+ throw new Error('sync.auto.intervalSeconds must be a positive number')
129
+ }
130
+ if (!Number.isFinite(minWriteIntervalSeconds) || minWriteIntervalSeconds < 0) {
131
+ throw new Error('sync.auto.minWriteIntervalSeconds must be >= 0')
132
+ }
133
+
134
+ const autoRemote = auto.remote ?? defaultRemote
135
+ if (!remotes[autoRemote]) {
136
+ throw new Error(
137
+ `sync.auto.remote \"${autoRemote}\" does not exist in sync.remotes. Available: ${remoteNames.join(', ')}`,
138
+ )
139
+ }
140
+
141
+ return {
142
+ remotes,
143
+ defaultRemote,
144
+ auto: {
145
+ enabled: auto.enabled ?? false,
146
+ intervalSeconds,
147
+ direction: auto.direction ?? 'push',
148
+ remote: autoRemote,
149
+ onWrite: auto.onWrite ?? false,
150
+ minWriteIntervalSeconds,
151
+ },
152
+ }
153
+ }
154
+
72
155
  // ── loadConfig — resolution chain ────────────────────────────────────────────
73
156
 
74
157
  const CONFIG_CANDIDATES = [
@@ -76,11 +159,21 @@ const CONFIG_CANDIDATES = [
76
159
  'suemo.config.js',
77
160
  ]
78
161
 
79
- const HOME_CONFIG = join(
80
- process.env.HOME ?? process.env.USERPROFILE ?? '~',
81
- '.suemo',
82
- 'suemo.ts',
83
- )
162
+ const log = getLogger(['suemo', 'config'])
163
+
164
+ function getHomeConfigPath(): string | null {
165
+ const home = process.env.HOME ?? process.env.USERPROFILE
166
+ if (!home) return null
167
+ return join(home, '.suemo', 'suemo.ts')
168
+ }
169
+
170
+ function resolveConfigPath(p: string): string {
171
+ if (p.startsWith('~/')) {
172
+ const home = process.env.HOME ?? process.env.USERPROFILE
173
+ if (home) return resolve(home, p.slice(2))
174
+ }
175
+ return resolve(p)
176
+ }
84
177
 
85
178
  export async function loadConfig(
86
179
  cwd = process.cwd(),
@@ -88,23 +181,40 @@ export async function loadConfig(
88
181
  ): Promise<SuemoConfig> {
89
182
  // 1. Explicit --config flag
90
183
  if (overridePath) {
91
- return importConfig(resolve(overridePath))
184
+ const resolved = resolveConfigPath(overridePath)
185
+ log.debug('Loading config from --config', { path: resolved })
186
+ return importConfig(resolved)
187
+ }
188
+ // 2. Environment override
189
+ const envConfigPath = process.env.SUEMO_CONFIG_PATH?.trim()
190
+ if (envConfigPath) {
191
+ const resolved = resolveConfigPath(envConfigPath)
192
+ log.debug('Loading config from SUEMO_CONFIG_PATH', { path: resolved })
193
+ return importConfig(resolved)
92
194
  }
93
- // 2. Project-local
195
+ // 3. Project-local
94
196
  for (const name of CONFIG_CANDIDATES) {
95
197
  const p = resolve(cwd, name)
96
- if (existsSync(p)) return importConfig(p)
198
+ if (existsSync(p)) {
199
+ log.debug('Loading project-local config', { path: p })
200
+ return importConfig(p)
201
+ }
202
+ }
203
+ // 4. User-level
204
+ const homeConfig = getHomeConfigPath()
205
+ if (homeConfig && existsSync(homeConfig)) {
206
+ log.debug('Loading user-level config', { path: homeConfig })
207
+ return importConfig(homeConfig)
97
208
  }
98
- // 3. User-level
99
- if (existsSync(HOME_CONFIG)) return importConfig(HOME_CONFIG)
100
209
 
101
210
  throw new Error(
102
211
  'No suemo config found.\n'
103
- + 'Run `suemo init` to create ~/.suemo/suemo.ts, or create suemo.config.ts in the project root.',
212
+ + 'Run `suemo init config` to create ~/.suemo/suemo.ts, set SUEMO_CONFIG_PATH, or create suemo.config.ts in the project root.',
104
213
  )
105
214
  }
106
215
 
107
216
  async function importConfig(path: string): Promise<SuemoConfig> {
217
+ log.debug('Importing config module', { path })
108
218
  const mod = await import(pathToFileURL(path).href)
109
219
  const cfg: unknown = mod.default ?? mod
110
220
  if (!cfg || typeof cfg !== 'object') {
@@ -12,18 +12,33 @@ export interface CompatibilityResult {
12
12
  errors: string[]
13
13
  }
14
14
 
15
+ export interface CompatibilityOptions {
16
+ /**
17
+ * Whether missing fn::embed() should fail compatibility.
18
+ * Keep true for runtime paths that execute vector/embed queries.
19
+ */
20
+ requireEmbedding?: boolean
21
+ /**
22
+ * Human-readable label for logging this preflight execution.
23
+ */
24
+ context?: string
25
+ }
26
+
15
27
  const MIN_MAJOR = 3
16
28
 
17
29
  export async function checkCompatibility(
18
30
  db: Surreal,
31
+ options: CompatibilityOptions = {},
19
32
  ): Promise<CompatibilityResult> {
20
33
  const errors: string[] = []
21
34
  let surrealVersion = 'unknown'
22
35
  let surrealkv = false
23
36
  let retention_ok = false
24
37
  let embedding = false
38
+ const requireEmbedding = options.requireEmbedding ?? true
39
+ const context = options.context ?? 'default'
25
40
 
26
- log.info('Running preflight compatibility checks')
41
+ log.info('Running preflight compatibility checks', { requireEmbedding, context })
27
42
 
28
43
  // ── Check 1: version string ───────────────────────────────────────────────
29
44
  try {
@@ -41,6 +56,7 @@ export async function checkCompatibility(
41
56
  )
42
57
  }
43
58
  }
59
+ log.debug('SurrealDB version check complete', { surrealVersion })
44
60
  } catch (e) {
45
61
  errors.push(`Cannot retrieve SurrealDB version: ${String(e)}`)
46
62
  }
@@ -54,6 +70,7 @@ export async function checkCompatibility(
54
70
  // if it was around then), not throw. An error means VERSION is unsupported.
55
71
  await db.query("SELECT * FROM suemo_preflight:probe VERSION d'2020-01-01T00:00:00Z'")
56
72
  surrealkv = true
73
+ log.debug('SurrealKV VERSION probe passed')
57
74
  } catch (e: unknown) {
58
75
  const msg = String(e).toLowerCase()
59
76
  if (msg.includes('version') || msg.includes('surrealkv') || msg.includes('not supported')) {
@@ -83,6 +100,7 @@ export async function checkCompatibility(
83
100
  await db.query('CREATE suemo_retention_probe:v SET t = time::now()')
84
101
  await db.query(`SELECT * FROM suemo_retention_probe:v VERSION d'${ninetyDaysAgo}'`)
85
102
  retention_ok = true
103
+ log.debug('Retention probe passed', { ninetyDaysAgo })
86
104
  } catch (e: unknown) {
87
105
  const msg = String(e).toLowerCase()
88
106
  if (msg.includes('retention') || msg.includes('version')) {
@@ -104,20 +122,28 @@ export async function checkCompatibility(
104
122
 
105
123
  // ── Check 4: fn::embed() resolves ────────────────────────────────────────
106
124
  // 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.
125
+ // An "Unknown function" error means embedding runtime is unavailable.
108
126
  try {
109
127
  // fn::embed requires the ML module and a configured model.
110
128
  // If it throws "No embedding model configured" that's acceptable — the
111
129
  // function exists. If it throws "Unknown function 'fn::embed'" → not available.
112
130
  await db.query(`RETURN fn::embed("suemo preflight test")`)
113
131
  embedding = true
132
+ log.debug('Embedding function probe passed')
114
133
  } catch (e: unknown) {
115
134
  const msg = String(e).toLowerCase()
116
135
  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
- )
136
+ embedding = false
137
+ if (requireEmbedding) {
138
+ errors.push(
139
+ 'fn::embed() is not available in this SurrealDB database. Import/configure a SurrealML embedding model for this namespace/database, then retry.',
140
+ )
141
+ } else {
142
+ log.warn('Embedding function unavailable; continuing due to non-strict preflight mode', {
143
+ context,
144
+ error: String(e),
145
+ })
146
+ }
121
147
  } else {
122
148
  // Other error (e.g. no model configured but function exists) — treat as available
123
149
  embedding = true
@@ -13,6 +13,7 @@ DEFINE FIELD OVERWRITE content ON memory TYPE string;
13
13
  DEFINE FIELD OVERWRITE summary ON memory TYPE option<string>;
14
14
  DEFINE FIELD OVERWRITE tags ON memory TYPE set<string> DEFAULT {};
15
15
  DEFINE FIELD OVERWRITE scope ON memory TYPE option<string>;
16
+ DEFINE FIELD OVERWRITE topic_key ON memory TYPE option<string> DEFAULT NONE;
16
17
  DEFINE FIELD OVERWRITE embedding ON memory TYPE array<float> DEFAULT [];
17
18
  DEFINE FIELD OVERWRITE confidence ON memory TYPE float DEFAULT 1.0;
18
19
  DEFINE FIELD OVERWRITE salience ON memory TYPE float DEFAULT 0.5;
@@ -24,33 +25,37 @@ DEFINE FIELD OVERWRITE valid_from ON memory TYPE datetime DEFAULT time::
24
25
  DEFINE FIELD OVERWRITE valid_until ON memory TYPE option<datetime> DEFAULT NONE;
25
26
 
26
27
  DEFINE FIELD OVERWRITE source ON memory TYPE option<string>;
28
+ DEFINE FIELD OVERWRITE prompt_source ON memory TYPE option<record<memory>> DEFAULT NONE;
27
29
  DEFINE FIELD OVERWRITE created_at ON memory TYPE datetime DEFAULT time::now();
28
30
  DEFINE FIELD OVERWRITE updated_at ON memory TYPE datetime DEFAULT time::now();
29
31
  DEFINE FIELD OVERWRITE consolidated ON memory TYPE bool DEFAULT false;
30
32
 
31
33
  -- REFERENCES: bidirectional record link (SurrealDB v3.0)
32
34
  DEFINE FIELD OVERWRITE consolidated_into ON memory
33
- TYPE option<record<memory>> REFERENCES;
35
+ TYPE option<record<memory>> REFERENCE;
34
36
 
35
37
  -- FSRS spaced-repetition fields (all optional; populated on first recall())
36
38
  DEFINE FIELD OVERWRITE fsrs_stability ON memory TYPE option<float>;
37
39
  DEFINE FIELD OVERWRITE fsrs_difficulty ON memory TYPE option<float>;
38
40
  DEFINE FIELD OVERWRITE fsrs_next_review ON memory TYPE option<datetime>;
39
41
 
40
- -- Vector index: HNSW, DEFER so init does not block on large existing datasets
42
+ -- Vector index: HNSW (keep syntax compatible with current SurrealDB target)
41
43
  DEFINE INDEX OVERWRITE idx_memory_embedding
42
44
  ON memory FIELDS embedding
43
- HNSW DIMENSION 1536 DIST COSINE
44
- DEFER;
45
+ HNSW DIMENSION 384 DIST COSINE;
45
46
 
46
47
  -- Full-text index: BM25 over content + summary
47
48
  DEFINE ANALYZER OVERWRITE suemo_analyzer
48
49
  TOKENIZERS blank,class
49
50
  FILTERS lowercase,snowball(english);
50
51
 
51
- DEFINE INDEX OVERWRITE idx_memory_fts
52
- ON memory FIELDS content, summary
53
- SEARCH ANALYZER suemo_analyzer BM25;
52
+ DEFINE INDEX OVERWRITE idx_memory_content_fts
53
+ ON memory FIELDS content
54
+ FULLTEXT ANALYZER suemo_analyzer BM25;
55
+
56
+ DEFINE INDEX OVERWRITE idx_memory_summary_fts
57
+ ON memory FIELDS summary
58
+ FULLTEXT ANALYZER suemo_analyzer BM25;
54
59
 
55
60
  -- Compound indexes for timeline and filter queries
56
61
  DEFINE INDEX OVERWRITE idx_memory_scope_time
@@ -62,6 +67,9 @@ DEFINE INDEX OVERWRITE idx_memory_kind_valid
62
67
  DEFINE INDEX OVERWRITE idx_memory_salience
63
68
  ON memory FIELDS salience;
64
69
 
70
+ DEFINE INDEX OVERWRITE idx_memory_topic_key
71
+ ON memory FIELDS topic_key;
72
+
65
73
  -- ── relates_to ───────────────────────────────────────────────────────────────
66
74
  DEFINE TABLE OVERWRITE relates_to SCHEMAFULL
67
75
  TYPE RELATION IN memory OUT memory;
@@ -73,6 +81,7 @@ DEFINE FIELD OVERWRITE strength ON relates_to TYPE float DEFAULT 0.5;
73
81
  DEFINE FIELD OVERWRITE valid_from ON relates_to TYPE datetime DEFAULT time::now();
74
82
  DEFINE FIELD OVERWRITE valid_until ON relates_to TYPE option<datetime> DEFAULT NONE;
75
83
  DEFINE FIELD OVERWRITE created_at ON relates_to TYPE datetime DEFAULT time::now();
84
+ DEFINE FIELD OVERWRITE updated_at ON relates_to TYPE datetime DEFAULT time::now();
76
85
 
77
86
  -- ── episode ───────────────────────────────────────────────────────────────────
78
87
  DEFINE TABLE OVERWRITE episode SCHEMAFULL;
@@ -81,10 +90,11 @@ DEFINE FIELD OVERWRITE session_id ON episode TYPE string;
81
90
  DEFINE FIELD OVERWRITE started_at ON episode TYPE datetime DEFAULT time::now();
82
91
  DEFINE FIELD OVERWRITE ended_at ON episode TYPE option<datetime> DEFAULT NONE;
83
92
  DEFINE FIELD OVERWRITE summary ON episode TYPE option<string>;
93
+ DEFINE FIELD OVERWRITE context ON episode TYPE option<object> DEFAULT NONE;
84
94
 
85
95
  -- REFERENCES: bidirectional array of memory links
86
96
  DEFINE FIELD OVERWRITE memory_ids ON episode
87
- TYPE array<record<memory>> REFERENCES
97
+ TYPE array<record<memory>> REFERENCE
88
98
  DEFAULT [];
89
99
 
90
100
  -- ── consolidation_run ────────────────────────────────────────────────────────
@@ -102,8 +112,19 @@ DEFINE FIELD OVERWRITE error ON consolidation_run TYPE option<string> DEF
102
112
 
103
113
  -- ── sync_cursor ───────────────────────────────────────────────────────────────
104
114
  -- One record per (remote.url, remote.ns, remote.db) triple.
105
- -- Stores the last successfully synced created_at timestamp.
115
+ -- Stores per-remote cursors for push/pull sync based on updated_at.
106
116
  DEFINE TABLE OVERWRITE sync_cursor SCHEMAFULL;
107
117
  DEFINE FIELD OVERWRITE remote_key ON sync_cursor TYPE string; -- sha1(url+ns+db)
108
118
  DEFINE FIELD OVERWRITE cursor ON sync_cursor TYPE datetime;
119
+ DEFINE FIELD OVERWRITE push_cursor ON sync_cursor TYPE datetime DEFAULT d'1970-01-01T00:00:00Z';
120
+ DEFINE FIELD OVERWRITE pull_cursor ON sync_cursor TYPE datetime DEFAULT d'1970-01-01T00:00:00Z';
109
121
  DEFINE FIELD OVERWRITE last_synced ON sync_cursor TYPE datetime DEFAULT time::now();
122
+
123
+ -- ── suemo_stats ──────────────────────────────────────────────────────────────
124
+ DEFINE TABLE OVERWRITE suemo_stats SCHEMAFULL;
125
+ DEFINE FIELD OVERWRITE ns_db ON suemo_stats TYPE string;
126
+ DEFINE FIELD OVERWRITE total_writes ON suemo_stats TYPE int DEFAULT 0;
127
+ DEFINE FIELD OVERWRITE total_queries ON suemo_stats TYPE int DEFAULT 0;
128
+ DEFINE FIELD OVERWRITE last_write ON suemo_stats TYPE option<datetime> DEFAULT NONE;
129
+ DEFINE FIELD OVERWRITE last_query ON suemo_stats TYPE option<datetime> DEFAULT NONE;
130
+ DEFINE INDEX OVERWRITE idx_stats_ns ON suemo_stats FIELDS ns_db;
package/src/db/schema.ts CHANGED
@@ -11,12 +11,15 @@ const log = getLogger(['suemo', 'db', 'schema'])
11
11
  export async function runSchema(db: Surreal): Promise<void> {
12
12
  log.info('Running schema migrations')
13
13
  const statements = SCHEMA.split(/;\s*\n/).map((s) => s.trim()).filter(Boolean)
14
- for (const stmt of statements) {
14
+ log.debug('Prepared schema statements', { count: statements.length, schemaBytes: SCHEMA.length })
15
+ for (const [index, stmt] of statements.entries()) {
15
16
  try {
17
+ const snippet = stmt.length > 160 ? `${stmt.slice(0, 160)}…` : stmt
18
+ log.debug('Executing schema statement', { index, snippet })
16
19
  await db.query(stmt)
17
- log.debug('Schema statement OK', { stmt: stmt.slice(0, 60) })
20
+ log.debug('Schema statement OK', { index, stmt: stmt.slice(0, 60) })
18
21
  } catch (e) {
19
- log.error('Schema statement failed', { stmt, error: String(e) })
22
+ log.error('Schema statement failed', { index, stmt, error: String(e) })
20
23
  throw e
21
24
  }
22
25
  }
@@ -0,0 +1,52 @@
1
+ import type { EmbeddingProvider } from '../config.ts'
2
+ import { getLogger } from '../logger.ts'
3
+ import { embedText } from './openai-compatible.ts'
4
+
5
+ const log = getLogger(['suemo', 'embedding'])
6
+
7
+ export interface EmbeddingResult {
8
+ clause: string
9
+ param?: number[]
10
+ }
11
+
12
+ export async function getEmbedding(
13
+ text: string,
14
+ config: EmbeddingProvider,
15
+ ): Promise<EmbeddingResult> {
16
+ log.debug('getEmbedding()', { provider: config.provider, dimension: config.dimension })
17
+
18
+ switch (config.provider) {
19
+ case 'surreal':
20
+ return { clause: 'fn::embed($content)' }
21
+
22
+ case 'openai-compatible': {
23
+ const vec = await embedText(text, config)
24
+ if (vec.length !== config.dimension) {
25
+ throw new Error(
26
+ `Embedding dimension mismatch: expected ${config.dimension}, got ${vec.length}`,
27
+ )
28
+ }
29
+ return { clause: '$embedding', param: vec }
30
+ }
31
+
32
+ case 'stub': {
33
+ const vec = new Array(config.dimension).fill(0)
34
+ log.debug('stub embedding: returning zero vector', { dimension: config.dimension })
35
+ return { clause: '$embedding', param: vec }
36
+ }
37
+ }
38
+ }
39
+
40
+ export function buildEmbeddingClause(config: EmbeddingProvider): string {
41
+ switch (config.provider) {
42
+ case 'surreal':
43
+ return 'fn::embed($content)'
44
+ case 'openai-compatible':
45
+ case 'stub':
46
+ return '$embedding'
47
+ }
48
+ }
49
+
50
+ export function getZeroVector(dimension: number): number[] {
51
+ return new Array(dimension).fill(0)
52
+ }
@@ -0,0 +1,43 @@
1
+ // src/embedding/openai_compatible.ts
2
+ import { getLogger } from "../logger.ts";
3
+
4
+ const log = getLogger(["suemo", "embedding", "openai-compatible"]);
5
+
6
+ export interface OpenAICompatibleEmbeddingConfig {
7
+ url: string; // e.g. http://127.0.0.1:8080/v1/embeddings
8
+ model: string;
9
+ dimension: number;
10
+ apiKey?: string;
11
+ }
12
+
13
+ export async function embedText(
14
+ text: string,
15
+ config: OpenAICompatibleEmbeddingConfig,
16
+ ): Promise<number[]> {
17
+ log.debug("embedText()", { textPreview: text.slice(0, 60) });
18
+
19
+ const res = await fetch(config.url, {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json",
23
+ "Authorization": `Bearer ${config.apiKey ?? "local"}`,
24
+ },
25
+ body: JSON.stringify({ input: text, model: config.model }),
26
+ });
27
+
28
+ if (!res.ok) {
29
+ throw new Error(`Embedding server error ${res.status}: ${await res.text()}`);
30
+ }
31
+
32
+ const json = await res.json() as {
33
+ data: { embedding: number[]; index: number }[];
34
+ };
35
+
36
+ const vec = json.data[0]?.embedding;
37
+ if (!vec) throw new Error("Embedding server returned empty data");
38
+ if (vec.length !== config.dimension) {
39
+ throw new Error(`Dimension mismatch: expected ${config.dimension}, got ${vec.length}`);
40
+ }
41
+
42
+ return vec;
43
+ }
package/src/goal.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Surreal } from 'surrealdb'
2
+ import type { SuemoConfig } from './config.ts'
2
3
  import { getLogger } from './logger.ts'
3
4
  import { invalidate, observe } from './memory/write.ts'
4
5
  import type { MemoryNode } from './types.ts'
@@ -8,10 +9,11 @@ const log = getLogger(['suemo', 'goal'])
8
9
  export async function goalSet(
9
10
  db: Surreal,
10
11
  content: string,
12
+ config: SuemoConfig,
11
13
  opts: { scope?: string; tags?: string[] } = {},
12
14
  ): Promise<MemoryNode> {
13
15
  log.info('goalSet()', { content: content.slice(0, 60) })
14
- return observe(db, { content, kind: 'goal', ...opts })
16
+ return observe(db, { content, kind: 'goal', ...opts }, config)
15
17
  }
16
18
 
17
19
  export async function goalResolve(db: Surreal, goalId: string): Promise<void> {
package/src/index.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  // src/index.ts — public API surface
2
- export { defineConfig, loadConfig } from './config.ts'
2
+ export { defineConfig, loadConfig, resolveSyncConfig } from './config.ts'
3
3
  export type {
4
4
  AuthConfig,
5
5
  ConsolidationConfig,
6
6
  EmbeddingProvider,
7
7
  LLMConfig,
8
8
  McpConfig,
9
+ ResolvedSyncAutoConfig,
10
+ ResolvedSyncConfig,
9
11
  RetrievalConfig,
10
12
  SuemoConfig,
11
13
  SurrealTarget,
14
+ SyncAutoConfig,
12
15
  SyncConfig,
16
+ SyncDirectionConfig,
13
17
  } from './config.ts'