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
@@ -0,0 +1,232 @@
1
+ import type { Surreal } from 'surrealdb'
2
+ import { z } from 'zod'
3
+ import { consolidate } from '../cognitive/consolidate.ts'
4
+ import { healthReport, suemoStats, vitals } from '../cognitive/health.ts'
5
+ import type { SuemoConfig } from '../config.ts'
6
+ import { goalList, goalResolve, goalSet } from '../goal.ts'
7
+ import { getLogger } from '../logger.ts'
8
+ import { episodeEnd, episodeStart, getSessionContext, setSessionContext } from '../memory/episode.ts'
9
+ import { query, recall, timeline, wander } from '../memory/read.ts'
10
+ import { believe, capturePrompt, invalidate, observe, upsertByKey } from '../memory/write.ts'
11
+ import { ObserveInputSchema, QueryInputSchema } from '../types.ts'
12
+
13
+ const log = getLogger(['suemo', 'mcp', 'dispatch'])
14
+
15
+ const MUTATING_TOOLS = new Set([
16
+ 'observe',
17
+ 'believe',
18
+ 'invalidate',
19
+ 'goal_set',
20
+ 'goal_resolve',
21
+ 'upsert_by_key',
22
+ 'capture_prompt',
23
+ 'session_context_set',
24
+ ])
25
+
26
+ interface DispatchOptions {
27
+ onMutation?: (tool: string) => Promise<void>
28
+ }
29
+
30
+ export async function handleToolCall(
31
+ db: Surreal,
32
+ config: SuemoConfig,
33
+ method: string,
34
+ params: Record<string, unknown>,
35
+ opts: DispatchOptions = {},
36
+ ): Promise<unknown> {
37
+ log.debug('Dispatching MCP tool call', { method, paramKeys: Object.keys(params) })
38
+
39
+ const maybeTriggerMutationSync = (): void => {
40
+ if (!MUTATING_TOOLS.has(method) || !opts.onMutation) return
41
+ void opts.onMutation(method).catch((error) => {
42
+ log.warning('Post-mutation hook failed', { method, error: String(error) })
43
+ })
44
+ }
45
+
46
+ switch (method) {
47
+ case 'observe': {
48
+ const result = await observe(db, ObserveInputSchema.parse(params), config)
49
+ maybeTriggerMutationSync()
50
+ return result
51
+ }
52
+
53
+ case 'believe': {
54
+ const parsed = z
55
+ .object({
56
+ content: z.string(),
57
+ scope: z.string().optional(),
58
+ confidence: z.number().optional(),
59
+ })
60
+ .parse(params)
61
+ const result = await believe(db, parsed, config)
62
+ maybeTriggerMutationSync()
63
+ return result
64
+ }
65
+
66
+ case 'invalidate': {
67
+ const parsed = z.object({ nodeId: z.string(), reason: z.string().optional() }).parse(params)
68
+ await invalidate(db, parsed.nodeId, parsed.reason)
69
+ maybeTriggerMutationSync()
70
+ return { ok: true }
71
+ }
72
+
73
+ case 'query':
74
+ return query(db, QueryInputSchema.parse(params), config)
75
+
76
+ case 'recall': {
77
+ const parsed = z.object({ nodeId: z.string() }).parse(params)
78
+ return recall(db, parsed.nodeId)
79
+ }
80
+
81
+ case 'wander': {
82
+ const parsed = z
83
+ .object({ anchor: z.string().optional(), hops: z.number().optional(), scope: z.string().optional() })
84
+ .parse(params)
85
+ return wander(db, {
86
+ ...(parsed.anchor ? { anchor: parsed.anchor } : {}),
87
+ ...(parsed.hops ? { hops: parsed.hops } : {}),
88
+ ...(parsed.scope ? { scope: parsed.scope } : {}),
89
+ })
90
+ }
91
+
92
+ case 'timeline': {
93
+ const parsed = z
94
+ .object({
95
+ scope: z.string().optional(),
96
+ from: z.string().optional(),
97
+ until: z.string().optional(),
98
+ limit: z.number().optional(),
99
+ })
100
+ .parse(params)
101
+ return timeline(db, {
102
+ ...(parsed.scope ? { scope: parsed.scope } : {}),
103
+ ...(parsed.from ? { from: parsed.from } : {}),
104
+ ...(parsed.until ? { until: parsed.until } : {}),
105
+ ...(parsed.limit ? { limit: parsed.limit } : {}),
106
+ })
107
+ }
108
+
109
+ case 'episode_start': {
110
+ const parsed = z.object({ sessionId: z.string() }).parse(params)
111
+ return episodeStart(db, parsed.sessionId)
112
+ }
113
+
114
+ case 'episode_end': {
115
+ const parsed = z.object({ sessionId: z.string(), summary: z.string().optional() }).parse(params)
116
+ return episodeEnd(db, parsed.sessionId, parsed.summary)
117
+ }
118
+
119
+ case 'goal_set': {
120
+ const parsed = z
121
+ .object({ content: z.string(), scope: z.string().optional(), tags: z.array(z.string()).optional() })
122
+ .parse(params)
123
+ const result = await goalSet(db, parsed.content, config, {
124
+ ...(parsed.scope ? { scope: parsed.scope } : {}),
125
+ tags: parsed.tags ?? [],
126
+ })
127
+ maybeTriggerMutationSync()
128
+ return result
129
+ }
130
+
131
+ case 'goal_resolve': {
132
+ const parsed = z.object({ goalId: z.string() }).parse(params)
133
+ await goalResolve(db, parsed.goalId)
134
+ maybeTriggerMutationSync()
135
+ return { ok: true }
136
+ }
137
+
138
+ case 'goal_list': {
139
+ const parsed = z.object({ scope: z.string().optional(), includeResolved: z.boolean().optional() }).parse(params)
140
+ return goalList(db, {
141
+ ...(parsed.scope ? { scope: parsed.scope } : {}),
142
+ includeResolved: parsed.includeResolved ?? false,
143
+ })
144
+ }
145
+
146
+ case 'health':
147
+ return healthReport(db)
148
+
149
+ case 'stats':
150
+ return suemoStats(db)
151
+
152
+ case 'vitals':
153
+ return vitals(db)
154
+
155
+ case 'consolidate': {
156
+ const parsed = z.object({ nremOnly: z.boolean().optional() }).parse(params)
157
+ return consolidate(db, {
158
+ nremOnly: parsed.nremOnly ?? false,
159
+ nremSimilarityThreshold: config.consolidation.nremSimilarityThreshold,
160
+ remRelationThreshold: config.consolidation.remRelationThreshold,
161
+ llm: config.consolidation.llm,
162
+ embedding: config.embedding,
163
+ })
164
+ }
165
+
166
+ case 'upsert_by_key': {
167
+ const parsed = z
168
+ .object({
169
+ topicKey: z.string(),
170
+ content: z.string(),
171
+ scope: z.string().optional(),
172
+ tags: z.array(z.string()).optional(),
173
+ confidence: z.number().optional(),
174
+ source: z.string().optional(),
175
+ sessionId: z.string().optional(),
176
+ kind: z.enum(['observation', 'belief', 'question', 'hypothesis', 'goal']).optional(),
177
+ })
178
+ .parse(params)
179
+ const result = await upsertByKey(db, config, parsed.topicKey, parsed.content, {
180
+ ...(parsed.scope ? { scope: parsed.scope } : {}),
181
+ tags: parsed.tags ?? [],
182
+ ...(parsed.source ? { source: parsed.source } : {}),
183
+ ...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
184
+ confidence: parsed.confidence ?? 1.0,
185
+ ...(parsed.kind ? { kind: parsed.kind } : {}),
186
+ })
187
+ maybeTriggerMutationSync()
188
+ return result
189
+ }
190
+
191
+ case 'capture_prompt': {
192
+ const parsed = z
193
+ .object({
194
+ prompt: z.string(),
195
+ derivedIds: z.array(z.string()).optional(),
196
+ scope: z.string().optional(),
197
+ sessionId: z.string().optional(),
198
+ })
199
+ .parse(params)
200
+ const result = await capturePrompt(db, config, parsed.prompt, parsed.derivedIds ?? [], {
201
+ ...(parsed.scope ? { scope: parsed.scope } : {}),
202
+ ...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
203
+ })
204
+ maybeTriggerMutationSync()
205
+ return result
206
+ }
207
+
208
+ case 'session_context_get': {
209
+ const parsed = z.object({ sessionId: z.string() }).parse(params)
210
+ return getSessionContext(db, parsed.sessionId)
211
+ }
212
+
213
+ case 'session_context_set': {
214
+ const parsed = z
215
+ .object({
216
+ sessionId: z.string(),
217
+ summary: z.string().optional(),
218
+ context: z.record(z.string(), z.unknown()).optional(),
219
+ })
220
+ .parse(params)
221
+ await setSessionContext(db, parsed.sessionId, {
222
+ ...(parsed.summary !== undefined ? { summary: parsed.summary } : {}),
223
+ ...(parsed.context !== undefined ? { context: parsed.context } : {}),
224
+ })
225
+ maybeTriggerMutationSync()
226
+ return { ok: true }
227
+ }
228
+
229
+ default:
230
+ throw new Error(`Unknown MCP tool: ${method}`)
231
+ }
232
+ }
package/src/mcp/server.ts CHANGED
@@ -1,23 +1,169 @@
1
1
  // src/mcp/server.ts
2
2
  import { Elysia } from 'elysia'
3
- import type { SuemoConfig } from '../config.ts'
4
- import { connect } from '../db/client.ts'
5
- import { requireCompatibility } from '../db/preflight.ts'
3
+ import { resolveSyncConfig, type SuemoConfig } from '../config.ts'
4
+ import { connect, disconnect } from '../db/client.ts'
5
+ import { checkCompatibility, requireCompatibility } from '../db/preflight.ts'
6
6
  import { runSchema } from '../db/schema.ts'
7
7
  import { getLogger } from '../logger.ts'
8
+ import { syncTo } from '../sync.ts'
9
+ import { runStdioServer } from './stdio.ts'
8
10
  import { buildMcpRouter } from './tools.ts'
9
11
 
10
12
  const log = getLogger(['suemo', 'mcp'])
11
13
 
14
+ interface AutoSyncOptions {
15
+ reason: 'timer' | 'write'
16
+ force?: boolean
17
+ }
18
+
19
+ function createAutoSyncRunner(
20
+ db: ReturnType<typeof connect> extends Promise<infer T> ? T : never,
21
+ config: SuemoConfig,
22
+ ): {
23
+ start: () => void
24
+ onWrite: (tool: string) => Promise<void>
25
+ stop: () => void
26
+ } {
27
+ const resolvedSync = resolveSyncConfig(config)
28
+ if (!resolvedSync) {
29
+ return {
30
+ start: () => {},
31
+ onWrite: async () => {},
32
+ stop: () => {},
33
+ }
34
+ }
35
+
36
+ let timer: ReturnType<typeof setInterval> | null = null
37
+ let running = false
38
+ let queued = false
39
+ let lastRunAt = 0
40
+
41
+ const runAutoSync = async ({ reason, force = false }: AutoSyncOptions): Promise<void> => {
42
+ if (!resolvedSync.auto.enabled) return
43
+ if (reason === 'write' && !resolvedSync.auto.onWrite) return
44
+
45
+ const now = Date.now()
46
+ const minIntervalMs = resolvedSync.auto.minWriteIntervalSeconds * 1000
47
+ if (!force && reason === 'write' && lastRunAt > 0 && now - lastRunAt < minIntervalMs) {
48
+ return
49
+ }
50
+
51
+ if (running) {
52
+ queued = true
53
+ return
54
+ }
55
+
56
+ const target = resolvedSync.remotes[resolvedSync.auto.remote]
57
+ if (!target) {
58
+ log.warning('Auto-sync skipped: remote missing', { remote: resolvedSync.auto.remote, reason })
59
+ return
60
+ }
61
+
62
+ running = true
63
+ try {
64
+ const result = await syncTo(db, target, {
65
+ direction: resolvedSync.auto.direction,
66
+ })
67
+ lastRunAt = Date.now()
68
+ log.info('Auto-sync completed', {
69
+ reason,
70
+ remote: resolvedSync.auto.remote,
71
+ direction: resolvedSync.auto.direction,
72
+ pushed: result.pushed,
73
+ errors: result.errors,
74
+ })
75
+ } catch (error) {
76
+ log.warning('Auto-sync failed', { reason, error: String(error) })
77
+ } finally {
78
+ running = false
79
+ if (queued) {
80
+ queued = false
81
+ queueMicrotask(() => {
82
+ void runAutoSync({ reason: 'write', force: true })
83
+ })
84
+ }
85
+ }
86
+ }
87
+
88
+ const start = (): void => {
89
+ if (!resolvedSync.auto.enabled) return
90
+ if (timer) return
91
+ const intervalMs = resolvedSync.auto.intervalSeconds * 1000
92
+ timer = setInterval(() => {
93
+ void runAutoSync({ reason: 'timer' })
94
+ }, intervalMs)
95
+ log.info('Auto-sync timer started', {
96
+ intervalSeconds: resolvedSync.auto.intervalSeconds,
97
+ remote: resolvedSync.auto.remote,
98
+ direction: resolvedSync.auto.direction,
99
+ })
100
+ }
101
+
102
+ const stop = (): void => {
103
+ if (!timer) return
104
+ clearInterval(timer)
105
+ timer = null
106
+ log.info('Auto-sync timer stopped')
107
+ }
108
+
109
+ const onWrite = async (tool: string): Promise<void> => {
110
+ if (!resolvedSync.auto.enabled || !resolvedSync.auto.onWrite) return
111
+ log.debug('Auto-sync write trigger', { tool })
112
+ await runAutoSync({ reason: 'write' })
113
+ }
114
+
115
+ return { start, onWrite, stop }
116
+ }
117
+
12
118
  export async function startMcpServer(config: SuemoConfig): Promise<void> {
13
119
  const db = await connect(config.surreal)
14
120
  await requireCompatibility(db)
15
121
  await runSchema(db)
122
+ const autoSync = createAutoSyncRunner(db, config)
123
+ autoSync.start()
124
+
125
+ const shutdown = async (): Promise<void> => {
126
+ autoSync.stop()
127
+ await disconnect()
128
+ process.exit(0)
129
+ }
130
+
131
+ process.once('SIGINT', () => {
132
+ void shutdown()
133
+ })
134
+ process.once('SIGTERM', () => {
135
+ void shutdown()
136
+ })
16
137
 
17
138
  new Elysia()
18
- .use(buildMcpRouter(db, config))
139
+ .use(buildMcpRouter(db, config, { onMutation: autoSync.onWrite }))
19
140
  .get('/health', () => ({ status: 'ok' }))
20
141
  .listen({ port: config.mcp.port, hostname: config.mcp.host })
21
142
 
22
143
  log.info('MCP server listening', { port: config.mcp.port, host: config.mcp.host })
23
144
  }
145
+
146
+ export async function startMcpStdioServer(config: SuemoConfig): Promise<void> {
147
+ const db = await connect(config.surreal)
148
+ const autoSync = createAutoSyncRunner(db, config)
149
+ autoSync.start()
150
+ try {
151
+ const compat = await checkCompatibility(db, {
152
+ requireEmbedding: config.embedding.provider === 'surreal',
153
+ context: 'mcp:stdio-startup',
154
+ })
155
+ if (!compat.ok) {
156
+ console.error('\n[suemo] Compatibility check failed:\n')
157
+ for (const err of compat.errors) {
158
+ console.error(` ✗ ${err}`)
159
+ }
160
+ console.error('\nFix the issues above and retry.\n')
161
+ process.exit(1)
162
+ }
163
+ await runSchema(db)
164
+ await runStdioServer(db, config, { onMutation: autoSync.onWrite })
165
+ } finally {
166
+ autoSync.stop()
167
+ await disconnect()
168
+ }
169
+ }